// AccountCenterUITestHarness.swift // OSGKeyboard ยท Main App // // Deterministic account and StoreKit UI integration harness. It exercises the // production coordinator and views while keeping Apple and backend services // outside XCUITest. #if DEBUG import OSGKeyboardShared import SwiftUI @MainActor struct AccountCenterUITestHarness: View { @StateObject private var coordinator: AccountSessionCoordinator private let startsSignIn: Bool init() { let startsSignIn = ProcessInfo.processInfo.arguments.contains( "--account-signin-loading" ) let service = AccountCenterUITestService( shouldFailSnapshot: ProcessInfo.processInfo.arguments.contains( "--account-snapshot-failure" ), startsSignedOut: startsSignIn, signInDelay: startsSignIn ? .seconds(5) : .zero ) self.startsSignIn = startsSignIn let store = AccountCenterUITestCreditStore(accountID: service.account.accountID) _coordinator = StateObject( wrappedValue: AccountSessionCoordinator( dependencies: AccountDependencies( sessionService: service, centerService: service, referralService: service ), creditStore: store ) ) } var body: some View { ThemedRoot { NavigationStack { AccountCenterView() .environmentObject(coordinator) } } .task { await coordinator.restoreIfNeeded() if startsSignIn { await coordinator.signIn( with: AppleAuthorizationPayload( identityToken: "ui-test-identity", authorizationCode: "ui-test-authorization", nonce: "ui-test-nonce" ) ) } } } } @MainActor struct ManagedCloudConsentUITestHarness: View { @ObservedObject private var config = ProviderConfig.shared @StateObject private var coordinator: AccountSessionCoordinator init() { let service = AccountCenterUITestService() let configuration = ProviderConfig.shared configuration.hasAcknowledgedCloudSharing = false configuration.credentialSource = .byok _coordinator = StateObject( wrappedValue: AccountSessionCoordinator( dependencies: AccountDependencies( sessionService: service, centerService: service, referralService: service ), creditStore: AccountCenterUITestCreditStore( accountID: service.account.accountID ) ) ) } var body: some View { ThemedRoot { NavigationStack { ScrollView { EnginePickerSection(config: config) .environmentObject(coordinator) .padding() } .navigationTitle("settings.aiService.title") } } .task { await coordinator.restoreIfNeeded() } } } private actor AccountCenterUITestService: AccountSessionServicing, AccountCenterServicing, ReferralProfileServicing { nonisolated let account = AccountSession( accountID: UUID(uuidString: "00000000-0000-0000-0000-000000000200")!, createdAtEpochSeconds: 1_700_000_000, displayName: "UI Test Account" ) private let shouldFailSnapshot: Bool private let startsSignedOut: Bool private let signInDelay: Duration init( shouldFailSnapshot: Bool = false, startsSignedOut: Bool = false, signInDelay: Duration = .zero ) { self.shouldFailSnapshot = shouldFailSnapshot self.startsSignedOut = startsSignedOut self.signInDelay = signInDelay } func restoreSession() async throws -> AccountSession? { startsSignedOut ? nil : account } func signIn(with payload: AppleAuthorizationPayload) async throws -> AccountSession { _ = payload if signInDelay > .zero { try await Task.sleep(for: signInDelay) } return account } func signOut() async throws {} func deleteAccount(with payload: AppleAuthorizationPayload) async throws { _ = payload } func loadAccountCenter() async throws -> AccountCenterSnapshot { if shouldFailSnapshot { throw AccountCenterUITestError.snapshotUnavailable } return AccountCenterSnapshot( account: account, credits: AccountCreditSummary(balance: 1_500, usedCredits: 500), referrals: [] ) } func updateDisplayName(_ displayName: String) async throws -> AccountSession { AccountSession( accountID: account.accountID, createdAtEpochSeconds: account.createdAtEpochSeconds, displayName: displayName ) } func redeemReferral(code: String) async throws { _ = code } func loadReferralProfile() async throws -> ReferralProfile { ReferralProfile( code: ReferralCode( code: "UITestInvite_123456789", inviteURL: URL(string: "https://osglab.com/i/UITestInvite_123456789")!, campaignID: nil, createdAt: Date(timeIntervalSince1970: 1_700_000_000) ), binding: nil ) } func loadCreditProducts() async throws -> [AccountCreditProduct] { [ AccountCreditProduct(productID: "500tks", credits: 500), AccountCreditProduct(productID: "1500tks", credits: 1_500), AccountCreditProduct(productID: "3000tks", credits: 3_000) ] } func submitCreditTransaction(_ signedTransaction: String) async throws -> AccountCreditPurchase { _ = signedTransaction return AccountCreditPurchase( transactionID: "2000000000200", productID: "500tks", creditsGranted: 500, balanceAfter: 2_000, replayed: false ) } func loadCreditPurchaseHistory( limit: Int, cursor: String? ) async throws -> AccountCreditPurchaseHistoryPage { _ = limit _ = cursor return AccountCreditPurchaseHistoryPage( items: [ AccountCreditPurchaseRecord( transactionID: "2000000000199", productID: "1500tks", creditsGranted: 1_500, balanceAfter: 1_500, purchasedAt: Date(timeIntervalSince1970: 1_700_000_000), status: .credited ) ], nextCursor: nil ) } } private enum AccountCenterUITestError: Error { case snapshotUnavailable } @MainActor private final class AccountCenterUITestCreditStore: AccountCreditStore { private let accountID: UUID private var transactions: [AccountStoreVerification] init(accountID: UUID) { self.accountID = accountID transactions = [ .verified( AccountStoreTransaction( id: 2_000_000_000_199, productID: "1500tks", appAccountToken: accountID, purchaseDate: Date(timeIntervalSince1970: 1_700_000_000), signedTransaction: String(repeating: "h", count: 100), finishOperation: {} ) ) ] } func product(for productID: String) async throws -> AccountStoreProduct? { let price: String switch productID { case "500tks": price = "$0.99" case "1500tks": price = "$1.99" case "3000tks": price = "$2.99" default: return nil } return AccountStoreProduct(id: productID, displayPrice: price) } func purchase( productID: String, accountID: UUID ) async throws -> AccountStorePurchaseOutcome { guard productID == "500tks", accountID == self.accountID else { return .success(.unverified) } let transaction = AccountStoreTransaction( id: 2_000_000_000_200, productID: productID, appAccountToken: accountID, purchaseDate: Date(), signedTransaction: String(repeating: "p", count: 100), finishOperation: {} ) let verification = AccountStoreVerification.verified(transaction) transactions.append(verification) return .success(verification) } func unfinishedTransactions() -> AsyncStream { AsyncStream { $0.finish() } } func transactionUpdates() -> AsyncStream { AsyncStream { $0.finish() } } func allTransactions() -> AsyncStream { let values = transactions return AsyncStream { continuation in values.forEach { continuation.yield($0) } continuation.finish() } } } #endif