Skip to content
Merged
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
74 changes: 50 additions & 24 deletions Sources/StytchCore/StartupClient.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import Combine
import Foundation

struct StartupClient {
public enum InitializationStatus: Sendable {
case success
case failure(errors: [Error])
}

enum StartupClient {
enum BootstrapRoute: BaseRouteType {
case fetch(Path)

Expand All @@ -19,11 +24,13 @@ struct StartupClient {

static var expectedClientType: ClientType?

static var isInitialized: AnyPublisher<Bool, Never> {
static var isInitialized: AnyPublisher<InitializationStatus, Never> {
isInitializedPublisher.eraseToAnyPublisher()
}

private static let isInitializedPublisher = PassthroughSubject<Bool, Never>()
private static let isInitializedPublisher = PassthroughSubject<InitializationStatus, Never>()
private static var bootstrapError: Error?
private static var sessionHydrationError: Error?

static func start() async throws {
if let expectedClientType {
Expand All @@ -40,9 +47,18 @@ struct StartupClient {
async let bootstrap: () = fetchAndApplyBootstrap()

// Await both tasks to complete
_ = try await (auth, bootstrap)

isInitializedPublisher.send(true)
do {
_ = try await (auth, bootstrap)
// We allow the calls to silently fail because they have safe fallbacks, but we want to let the developer know if something went wrong
let potentialErrors = [bootstrapError, sessionHydrationError].compactMap { $0 }
if !potentialErrors.isEmpty {
isInitializedPublisher.send(.failure(errors: potentialErrors))
} else {
isInitializedPublisher.send(.success)
}
} catch {
isInitializedPublisher.send(.failure(errors: [error]))
}

StytchConsoleLogger.log(message: "Stytch SDK initialized for client type: \(clientType)")
}
Expand All @@ -53,9 +69,19 @@ struct StartupClient {
}
switch clientType {
case .consumer:
_ = try? await StytchClient.sessions.authenticate(parameters: .init(sessionDurationMinutes: nil))
do {
_ = try await StytchClient.sessions.authenticate(parameters: .init(sessionDurationMinutes: nil))
sessionHydrationError = nil
} catch {
sessionHydrationError = error
}
case .b2b:
_ = try? await StytchB2BClient.sessions.authenticate(parameters: .init(sessionDurationMinutes: nil))
do {
_ = try await StytchB2BClient.sessions.authenticate(parameters: .init(sessionDurationMinutes: nil))
sessionHydrationError = nil
} catch {
sessionHydrationError = error
}
}
}

Expand Down Expand Up @@ -83,26 +109,26 @@ struct StartupClient {
}

@discardableResult static func bootstrap() async throws -> BootstrapResponseData {
guard let publicToken = StytchClient.stytchClientConfiguration?.publicToken else {
throw StytchSDKError.consumerSDKNotConfigured
}

// Attempt to fetch the latest bootstrap data from the API using the provided public token.
// If the network request succeeds, extract and use the wrapped response data.
// If the network request fails, fall back to the locally stored bootstrap data.
// If no local data exists, use a predefined default bootstrap data.
let bootstrapResponseData: BootstrapResponseData
if let bootstrapData = try? await router.get(route: .fetch(Path(rawValue: publicToken))) as BootstrapResponse {
bootstrapResponseData = bootstrapData.wrapped
} else if let currentBootstrapData = Current.localStorage.bootstrapData {
bootstrapResponseData = currentBootstrapData
} else {
bootstrapResponseData = BootstrapResponseData.defaultBootstrapData
do {
guard let publicToken = StytchClient.stytchClientConfiguration?.publicToken else {
throw StytchSDKError.consumerSDKNotConfigured
}
let updatedBootstrapData = try await router.get(route: .fetch(Path(rawValue: publicToken))) as BootstrapResponse
bootstrapError = nil
Current.localStorage.bootstrapData = updatedBootstrapData.wrapped
return updatedBootstrapData.wrapped
} catch {
bootstrapError = error
if let currentBootstrapData = Current.localStorage.bootstrapData {
return currentBootstrapData
} else {
Current.localStorage.bootstrapData = BootstrapResponseData.defaultBootstrapData
return BootstrapResponseData.defaultBootstrapData
}
}

// Update the local storage with the resolved bootstrap data before returning it.
Current.localStorage.bootstrapData = bootstrapResponseData

return bootstrapResponseData
}
}
4 changes: 2 additions & 2 deletions Sources/StytchCore/StytchClientCommon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ protocol StytchClientCommonInternal: StytchClientCommonPublic {
static var shared: Self { get }
static var clientType: ClientType { get }

static var isInitialized: AnyPublisher<Bool, Never> { get }
static var isInitialized: AnyPublisher<InitializationStatus, Never> { get }

static func handle(url: URL, sessionDurationMinutes: Minutes) async throws -> DeeplinkHandledStatus<DeeplinkResponse, DeeplinkTokenType, DeeplinkRedirectType>
}
Expand Down Expand Up @@ -135,7 +135,7 @@ public extension StytchClientCommonPublic {
1. Attempting to call sessions.authenticate (if there's a session token cached on the device).
2. Bootstrapping configuration, including DFP and captcha setup.
*/
static var isInitialized: AnyPublisher<Bool, Never> {
static var isInitialized: AnyPublisher<InitializationStatus, Never> {
StartupClient.isInitialized
}

Expand Down
Loading