Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ final class CBAuthenticationResource: CBAPIResource {
})
urlRequest.httpBody = bodyComponents.query?.data(using: .utf8)
urlRequest.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
urlRequest.addValue(authHeader!, forHTTPHeaderField: "Authorization")
if let resolvedAuthHeader = resolvedAuthHeader {
urlRequest.addValue(resolvedAuthHeader, forHTTPHeaderField: "Authorization")
}
urlRequest.addValue(sdkVersion, forHTTPHeaderField: "version")
urlRequest.addValue(platform, forHTTPHeaderField: "platform")
return urlRequest
Expand Down
64 changes: 63 additions & 1 deletion Chargebee/Classes/Configuration/CBEnvironment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

import Foundation

/// Closure supplied by the App, which is responsible for returning a fresh mobile token from the server.
/// The SDK invokes it during configure and whenever a request fails with a 401. The Server must mint a token via
/// `create_mobile_token` and return the raw token to the App.
public typealias CBMobileTokenProvider = (@escaping (String?) -> Void) -> Void

class CBEnvironment {
static var site: String = ""
static var apiKey: String = ""
Expand All @@ -14,7 +19,13 @@ class CBEnvironment {
static var version: CatalogVersion = .unknown
static var session = URLSession.shared
static var environment: String = "cb_ios_sdk"

// When a mobile token is present, the SDK sends it for the `Authorization` similar to the publishable key.
// If empty, we fall back to the publishable key flow.
static var mobileToken: String = ""
static var tokenProvider: CBMobileTokenProvider?
static var encodedMobileToken: String {
return mobileToken.data(using: .utf8)?.base64EncodedString() ?? ""
}

func configure(site: String, apiKey: String, allowErrorLogging: Bool, sdkKey: String? = nil) {
let resultHandler: CBAuthenticationHandler = { result in
Expand Down Expand Up @@ -65,4 +76,55 @@ class CBEnvironment {
}
}

// Mobile token flow: the SDK is configured without a publishable key. We fetch a token from
// the App's token provider and then verify the app details using it. If successful, the SDK is ready to use.
func configure(site: String, sdkKey: String? = nil, allowErrorLogging: Bool, tokenProvider: @escaping CBMobileTokenProvider, handler: @escaping CBAuthenticationHandler) {
CBEnvironment.site = site
CBEnvironment.apiKey = ""
CBEnvironment.encodedApiKey = ""
CBEnvironment.allowErrorLogging = allowErrorLogging
CBEnvironment.baseUrl = "https://\(site).chargebee.com/api"
CBEnvironment.version = .unknown
CBEnvironment.tokenProvider = tokenProvider
if let sdkKey = sdkKey {
CBEnvironment.sdkKey = sdkKey
}

let (onSuccess, onError) = CBResult.buildResultHandlers(handler, nil)
CBEnvironment.refreshMobileToken { success in
guard success else {
return onError(CBError.defaultSytemError(statusCode: 401, message: "Unable to fetch a mobile token from the token provider"))
}
guard CBEnvironment.sdkKey.isNotEmpty else {
// Nothing to verify without an SDK key; environment is ready.
return onSuccess(CBAuthenticationStatus(details: CBAuthentication(appId: nil, status: "ok", version: .unknown)))
Comment on lines +81 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reset authentication state on reconfiguration.

If this overload receives sdkKey: nil, it retains a previous CBEnvironment.sdkKey. If API-key configuration follows mobile-token configuration, mobileToken and tokenProvider remain set, so resolvedAuthHeader still selects the old mobile token. Assign sdkKey = sdkKey ?? "" here, and clear mobile-token state in the API-key configuration path.

As per path instructions, “focus solely on correctness and safety.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Chargebee/Classes/Configuration/CBEnvironment.swift` around lines 81 - 100,
Update configure so CBEnvironment.sdkKey is always reset from the current
optional value, using an empty value when sdkKey is nil, instead of retaining
prior state. In the API-key configuration path, also clear the existing
mobileToken and tokenProvider state so resolvedAuthHeader cannot reuse a
previous mobile-token configuration.

Source: Path instructions

}
CBAuthenticationManager().authenticate(forSDKKey: CBEnvironment.sdkKey) { result in
switch result {
case .success(let status):
CBEnvironment.version = status.details.version ?? .unknown
onSuccess(status)
case .error(let error):
CBEnvironment.version = .unknown
onError(error)
}
}
}
}

// Fetches a fresh token from the App's token provider and stores it.
static func refreshMobileToken(completion: @escaping (Bool) -> Void) {
guard let provider = tokenProvider else {
return completion(false)
}
provider { token in
if let token = token, token.isNotEmpty {
CBEnvironment.mobileToken = token
completion(true)
Comment on lines +116 to +123

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Synchronize mobile-token state.

The token-provider callback writes CBEnvironment.mobileToken while request construction and 401 retry handling read it on other execution paths. Concurrent mutable String access can send a stale or invalid Authorization header. Protect token reads and writes with one lock or serial state queue.

As per path instructions, “focus strictly on merge-blocking concerns.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Chargebee/Classes/Configuration/CBEnvironment.swift` around lines 116 - 123,
Synchronize all accesses to CBEnvironment.mobileToken across refreshMobileToken,
request construction, and 401 retry handling. Use one shared lock or serial
state queue for both token reads and writes, ensuring Authorization headers
observe a consistent token value without changing the existing refresh
completion behavior.

Source: Path instructions

} else {
completion(false)
}
}
}

}
8 changes: 8 additions & 0 deletions Chargebee/Classes/Configuration/Chargebee.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ public class Chargebee {
}
}
}

/// Configures the SDK using a mobile token instead of a publishable API key.
/// The `tokenProvider` is called to obtain a token from the merchant's backend, both now and
/// whenever a request is rejected with a 401 (expired/revoked token).
public static func configure(site: String, sdkKey: String? = nil, allowErrorLogging: Bool = true, tokenProvider: @escaping CBMobileTokenProvider, handler: @escaping CBAuthenticationHandler) {
CBEnvironment.environment = self.environment
CBEnvironment().configure(site: site, sdkKey: sdkKey, allowErrorLogging: allowErrorLogging, tokenProvider: tokenProvider, handler: handler)
}

public func retrieveSubscription(forSubscriptionID id: String, handler: @escaping CBSubscriptionHandler) {
let logger = CBLogger(name: "Subscription", action: "Fetch Subscription")
Expand Down
11 changes: 10 additions & 1 deletion Chargebee/Classes/Network/CBAPIRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,15 @@ extension CBAPIResource {
buildBaseRequest()
}

// When a mobile token is configured we send it as `Authorization: Basic base64(token)` for every
// request (same scheme as the publishable key). Otherwise we fall back to the resource's own header.
var resolvedAuthHeader: String? {
if CBEnvironment.mobileToken.isNotEmpty {
return "Basic \(CBEnvironment.encodedMobileToken)"
}
return authHeader
}

func create() -> URLRequest {
var urlRequest = buildBaseRequest()
urlRequest.httpMethod = "post"
Expand All @@ -64,7 +73,7 @@ extension CBAPIResource {
}

var urlRequest = URLRequest(url: components!.url!)
if let authHeader = authHeader {
if let authHeader = resolvedAuthHeader {
urlRequest.addValue(authHeader, forHTTPHeaderField: "Authorization")
}
header?.forEach({ (key, value) in
Expand Down
16 changes: 15 additions & 1 deletion Chargebee/Classes/Network/CBNetworkRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,27 @@ public protocol CBNetworkRequest {

@available(macCatalyst 13.0, *)
extension CBNetworkRequest {
func load(_ session: URLSession = URLSession.shared, urlRequest: URLRequest, withCompletion completion: SuccessHandler<ModelType>? = nil, onError: ErrorHandler? = nil) {
func load(_ session: URLSession = URLSession.shared, urlRequest: URLRequest, withCompletion completion: SuccessHandler<ModelType>? = nil, onError: ErrorHandler? = nil, allowRetry: Bool = true) {

let task = CBEnvironment.session.dataTask(with: urlRequest, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
if let error = error {
onError?(CBError.defaultSytemError(statusCode: 400, message: error.localizedDescription))
return
}
// Mobile token expired/revoked: fetch a fresh token from the merchant backend and retry once.
if let response = response as? HTTPURLResponse, response.statusCode == 401,
allowRetry, CBEnvironment.tokenProvider != nil {
CBEnvironment.refreshMobileToken { success in
guard success else {
onError?(self.buildCBError(data, statusCode: 401))
return
}
var retryRequest = urlRequest
retryRequest.setValue("Basic \(CBEnvironment.encodedMobileToken)", forHTTPHeaderField: "Authorization")
self.load(session, urlRequest: retryRequest, withCompletion: completion, onError: onError, allowRetry: false)
}
return
}
if let response = response as? HTTPURLResponse, response.statusCode >= 400 {
onError?(self.buildCBError(data, statusCode: response.statusCode))
return
Expand Down
28 changes: 25 additions & 3 deletions Example/Chargebee/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,32 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application
Chargebee.configure(site: "cb-abc-test",
apiKey: "API-KEY", sdkKey: "SDK-KEY", allowErrorLogging: false)
// Configure the SDK with a mobile token fetched from your server instead of
// embedding a publishable API key in the app.
Chargebee.configure(
site: "cb-abc-test",
sdkKey: "SDK-KEY",
tokenProvider: { completion in
// Ask your server for a fresh mobile token (it mints one via
// `create_mobile_token`), then hand the raw token back to the SDK.
AppDelegate.fetchMobileToken(completion: completion)
},
handler: { result in
switch result {
case .success(let status):
debugPrint("Chargebee configured: \(status)")
case .error(let error):
debugPrint("Chargebee configuration failed: \(error)")
}
}
)

return true
}

/// Stand-in for the call to your own server that returns a Chargebee mobile token.
/// Replace the body with a real network request to your server.
private static func fetchMobileToken(completion: @escaping (String?) -> Void) {
completion("cb_mob_replace_with_token_from_your_backend")
}
}
Loading