chore(release): bump version to 2.1.6 (build 131)

Version: CURRENT_PROJECT_VERSION 130 -> 131 (project.yml, AGENTS.md, READMEs,
docs/llms.txt).

fix(flow): host-app identification now compiles in every build configuration
(Debug, TestFlight, Release). The arbiter probe and HostAppIdentity were
DEBUG-only, so TestFlight/App Store builds never knew the host bundle id and
could not auto-return after a startflow handoff. Debug-only logging and the
Home test card are unchanged.

Also includes the pending work in the tree:
- Home: single account/setup card surface (HomeAccountSurfacePolicy/Views)
- Wide screens: voice key row and centred mic placement, iPhone Duo layout
- Keyboard: clipboard skill ranking and AI skill layout updates
- Debug: account session handoff to the Simulator (+ import script)
- Docs: portfolio assets, privacy policy URL, site/README refresh
This commit is contained in:
Rocky
2026-10-29 11:54:13 +08:00
parent 055a40976f
commit 4dce67df84
65 changed files with 3682 additions and 787 deletions
+51 -10
View File
@@ -141,6 +141,8 @@ final class AIAgentSkillLayoutTests: XCTestCase {
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID,
AIClipboardSkillCatalog.openLinkID,
AIClipboardSkillCatalog.summarizeWebPageID,
AIClipboardSkillCatalog.callPhoneID,
@@ -173,6 +175,8 @@ final class AIAgentSkillLayoutTests: XCTestCase {
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID,
AIClipboardSkillCatalog.openLinkID,
AIClipboardSkillCatalog.summarizeWebPageID,
AIClipboardSkillCatalog.callPhoneID,
@@ -205,6 +209,8 @@ final class AIAgentSkillLayoutTests: XCTestCase {
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID,
AIClipboardSkillCatalog.callPhoneID,
AIClipboardSkillCatalog.createContactID,
AIClipboardSkillCatalog.summarizeID,
@@ -240,6 +246,8 @@ final class AIAgentSkillLayoutTests: XCTestCase {
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.summarizeID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID,
AIClipboardSkillCatalog.organizeListID
]
)
@@ -247,7 +255,7 @@ final class AIAgentSkillLayoutTests: XCTestCase {
defaults.integer(
forKey: AppGroupConfiguration.Keys.agentSkillDefaultsMigrationVersion
),
11
12
)
}
@@ -272,6 +280,8 @@ final class AIAgentSkillLayoutTests: XCTestCase {
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID,
AIClipboardSkillCatalog.summarizeID,
AIClipboardSkillCatalog.organizeListID
]
@@ -306,12 +316,19 @@ final class AIAgentSkillLayoutTests: XCTestCase {
let migrated = AppGroupStore(defaults: defaults).agentSkillLayout
XCTAssertEqual(migrated.enabledIDs, [AIClipboardSkillCatalog.replyID])
XCTAssertEqual(
migrated.enabledIDs,
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID
]
)
XCTAssertEqual(
defaults.integer(
forKey: AppGroupConfiguration.Keys.agentSkillDefaultsMigrationVersion
),
11
12
)
}
@@ -332,16 +349,27 @@ final class AIAgentSkillLayoutTests: XCTestCase {
let store = AppGroupStore(defaults: defaults)
XCTAssertEqual(store.agentSkillLayout.enabledIDs, [AIClipboardSkillCatalog.translateID])
XCTAssertEqual(
store.agentSkillLayout.enabledIDs,
[
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID
]
)
XCTAssertEqual(
defaults.integer(
forKey: AppGroupConfiguration.Keys.agentSkillDefaultsMigrationVersion
),
11
12
)
XCTAssertEqual(
AppGroupStore(defaults: defaults).agentSkillLayout.enabledIDs,
[AIClipboardSkillCatalog.translateID]
[
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID
]
)
}
@@ -367,13 +395,18 @@ final class AIAgentSkillLayoutTests: XCTestCase {
XCTAssertEqual(
migrated.enabledIDs,
[AIClipboardSkillCatalog.replyID, AIClipboardSkillCatalog.translateID]
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID
]
)
XCTAssertEqual(
defaults.integer(
forKey: AppGroupConfiguration.Keys.agentSkillDefaultsMigrationVersion
),
11
12
)
}
@@ -400,7 +433,11 @@ final class AIAgentSkillLayoutTests: XCTestCase {
XCTAssertEqual(
AppGroupStore(defaults: enabledDefaults).agentSkillLayout.enabledIDs,
[AIClipboardSkillCatalog.replyID]
[
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID
]
)
let disabledDefaults = makeDefaults()
@@ -419,7 +456,11 @@ final class AIAgentSkillLayoutTests: XCTestCase {
)
XCTAssertEqual(
AppGroupStore(defaults: disabledDefaults).agentSkillLayout.enabledIDs,
[AIClipboardSkillCatalog.translateID]
[
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.translateToTargetID,
AIClipboardSkillCatalog.polishOriginalID
]
)
}
@@ -182,6 +182,64 @@ final class AIClipboardSkillTests: XCTestCase {
)
}
func testTranslateToTargetUsesConfiguredTargetLanguage() {
let prompt = AIClipboardSkillCatalog.instruction(
skillID: AIClipboardSkillCatalog.translateToTargetID,
locale: "en",
translationTargetLocaleId: "ja",
preferredLanguages: ["zh-Hans"]
)
XCTAssertTrue(prompt.contains("Japanese"))
XCTAssertFalse(prompt.contains("Chinese"))
}
func testTranslateToTargetPromptFallsBackWhenTargetIsOff() {
let prompt = AIClipboardSkillCatalog.instruction(
skillID: AIClipboardSkillCatalog.translateToTargetID,
locale: "en",
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId
)
XCTAssertTrue(prompt.contains("English"))
}
func testTranslateToTargetButtonTitleNamesTargetAndHidesWhenOff() {
XCTAssertEqual(
AIClipboardSkillCatalog.translateToTargetButtonTitle(
translationTargetLocaleId: "en",
uiLanguage: .chinese
),
"翻译英语"
)
XCTAssertEqual(
AIClipboardSkillCatalog.translateToTargetButtonTitle(
translationTargetLocaleId: "ja",
uiLanguage: .english
),
"To Japanese"
)
XCTAssertNil(
AIClipboardSkillCatalog.translateToTargetButtonTitle(
translationTargetLocaleId: TranslationLanguageCatalog.offLocaleId,
uiLanguage: .english
)
)
}
func testPolishKeepsOriginalLanguageAndCarriesNeverAnswerBoundary() throws {
let skill = try XCTUnwrap(
AIClipboardSkillCatalog.skill(id: AIClipboardSkillCatalog.polishOriginalID)
)
let prompt = AIClipboardSkillCatalog.instruction(
for: skill,
locale: "en",
translationTargetLocaleId: "ja"
)
XCTAssertTrue(prompt.contains("without changing its language"))
XCTAssertTrue(prompt.contains("grammar"))
XCTAssertTrue(prompt.contains("Non-negotiable boundary"))
XCTAssertFalse(prompt.contains("Japanese"))
}
func testSummarizeAsksForOverviewNotShortening() {
let prompt = AIClipboardSkillCatalog.instruction(
skillID: AIClipboardSkillCatalog.summarizeID,
@@ -133,6 +133,60 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
XCTAssertLessThan(try XCTUnwrap(translateIndex), try XCTUnwrap(speakAsMeIndex))
}
/// Device-language text + a configured target → "Translate to <target>",
/// and never the foreign-text actions (system translate / polish).
func testSystemLanguageTextOffersTranslateToTarget() {
let recommendations = ClipboardSkillSemanticRanker.recommended(
skills: Self.catalogWithoutOptInSkills,
sourceText: "明天下午三点开会,别迟到。",
analysis: analysis(language: "zh"),
uiLanguage: .chinese,
limit: 5,
preferredLanguages: ["zh-Hans-CN"],
translationTargetLocaleId: "en"
).map(\.id)
XCTAssertTrue(recommendations.contains(AIClipboardSkillCatalog.translateToTargetID))
XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.translateID))
XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.polishOriginalID))
}
func testTranslateToTargetIsHiddenWhenTranslationIsOffOrTargetMatchesText() {
for target in [TranslationLanguageCatalog.offLocaleId, "zh-Hans"] {
let recommendations = ClipboardSkillSemanticRanker.recommended(
skills: Self.catalogWithoutOptInSkills,
sourceText: "明天下午三点开会,别迟到。",
analysis: analysis(language: "zh"),
uiLanguage: .chinese,
limit: 5,
preferredLanguages: ["zh-Hans-CN"],
translationTargetLocaleId: target
).map(\.id)
XCTAssertFalse(
recommendations.contains(AIClipboardSkillCatalog.translateToTargetID),
"target \(target)"
)
}
}
/// Foreign text: system translate + polish, never translate-to-target.
func testForeignTextOffersPolishButNotTranslateToTarget() {
let recommendations = ClipboardSkillSemanticRanker.recommended(
skills: Self.catalogWithoutOptInSkills,
sourceText: "I has a meeting tomorrow, please dont be late.",
analysis: analysis(language: "en"),
uiLanguage: .chinese,
limit: 5,
preferredLanguages: ["zh-Hans-CN"],
translationTargetLocaleId: "ja"
).map(\.id)
XCTAssertTrue(recommendations.contains(AIClipboardSkillCatalog.translateID))
XCTAssertTrue(recommendations.contains(AIClipboardSkillCatalog.polishOriginalID))
XCTAssertFalse(recommendations.contains(AIClipboardSkillCatalog.translateToTargetID))
}
func testTranslationIsNotRecommendedForSystemLanguage() {
let recommendations = ClipboardSkillSemanticRanker.recommended(
skills: Self.catalogWithoutOptInSkills,
@@ -161,6 +215,7 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
recommendations,
[
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.polishOriginalID,
AIClipboardSkillCatalog.replyID
]
)
@@ -523,7 +578,8 @@ final class ClipboardSkillSemanticRankerTests: XCTestCase {
recommendations.map(\.id),
[
AIClipboardSkillCatalog.translateID,
AIClipboardSkillCatalog.replyID
AIClipboardSkillCatalog.replyID,
AIClipboardSkillCatalog.polishOriginalID
]
)
}
@@ -0,0 +1,121 @@
// DebugAccountSessionHandoffTests.swift
// OSGKeyboardTests
//
// DEBUG-only device → Simulator session handoff: the device must forget the
// session it hands off, and the Simulator must never keep the handoff file.
#if DEBUG
@testable import OSGKeyboard
@testable import OSGKeyboardHostSupport
import XCTest
final class DebugAccountSessionHandoffTests: XCTestCase {
private var directory: URL!
override func setUpWithError() throws {
directory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
}
override func tearDownWithError() throws {
try? FileManager.default.removeItem(at: directory)
}
// MARK: - AccountAPIClient
func testRelinquishReturnsSessionAndForgetsItEverywhere() async throws {
let session = makeAccountSession()
let store = InMemoryAccountSecurityStore(session: session)
let client = makeClient(store: store)
// Warm the in-memory cache: clearing Keychain alone would not stop it.
_ = try await client.currentSession()
let relinquished = try await client.debugRelinquishSession()
XCTAssertEqual(relinquished, session)
let stored = await store.session
XCTAssertNil(stored)
let current = try await client.currentSession()
XCTAssertNil(current)
}
func testRelinquishWithoutSessionThrows() async {
let client = makeClient(store: InMemoryAccountSecurityStore())
do {
_ = try await client.debugRelinquishSession()
XCTFail("Expected sessionUnavailable")
} catch {
XCTAssertEqual(error as? AccountAPIError, .sessionUnavailable)
}
}
func testAdoptReplacesCachedSession() async throws {
let store = InMemoryAccountSecurityStore(session: makeAccountSession())
let client = makeClient(store: store)
_ = try await client.currentSession()
let handedOff = makeAccountSession(accessToken: "access-device", refreshToken: "refresh-device")
try await client.debugAdoptSession(handedOff)
let current = try await client.currentSession()
XCTAssertEqual(current, handedOff)
let stored = await store.session
XCTAssertEqual(stored, handedOff)
}
// MARK: - Handoff file
func testExportFileRoundTripsThroughImportAndIsDeleted() throws {
let session = makeAccountSession()
let url = try DebugAccountSessionHandoff.writeExportFile(session, directory: directory)
let imported = DebugAccountSessionHandoff.consumePendingImport(directory: directory)
XCTAssertEqual(imported, session)
XCTAssertFalse(FileManager.default.fileExists(atPath: url.path))
}
func testMalformedImportIsRejectedAndStillDeleted() throws {
let url = directory.appendingPathComponent(DebugAccountSessionHandoff.fileName)
try Data(#"{"format":"something-else","session":{}}"#.utf8).write(to: url)
XCTAssertNil(DebugAccountSessionHandoff.consumePendingImport(directory: directory))
XCTAssertFalse(FileManager.default.fileExists(atPath: url.path))
}
func testParkedSessionIsPendingUntilConsumed() throws {
XCTAssertFalse(DebugAccountSessionHandoff.hasPendingImport(directory: directory))
_ = try DebugAccountSessionHandoff.writeExportFile(makeAccountSession(), directory: directory)
XCTAssertTrue(DebugAccountSessionHandoff.hasPendingImport(directory: directory))
_ = DebugAccountSessionHandoff.consumePendingImport(directory: directory)
XCTAssertFalse(DebugAccountSessionHandoff.hasPendingImport(directory: directory))
}
func testMissingImportReturnsNil() {
XCTAssertNil(DebugAccountSessionHandoff.consumePendingImport(directory: directory))
}
func testImportedMarkerMatchesOnlyTheImportedAccount() throws {
let suite = "DebugAccountSessionHandoffTests.\(UUID().uuidString)"
let defaults = try XCTUnwrap(UserDefaults(suiteName: suite))
defer { defaults.removePersistentDomain(forName: suite) }
let imported = makeAccountSession().accountId
DebugAccountSessionHandoff.markImported(accountID: imported, defaults: defaults)
XCTAssertTrue(DebugAccountSessionHandoff.isImported(accountID: imported, defaults: defaults))
XCTAssertFalse(DebugAccountSessionHandoff.isImported(accountID: UUID(), defaults: defaults))
}
private func makeClient(store: InMemoryAccountSecurityStore) -> AccountAPIClient {
AccountAPIClient(
baseURL: URL(string: "https://account.test")!,
transport: QueueAccountTransport([]),
sessionVault: store
)
}
}
#endif
@@ -82,40 +82,6 @@ final class FlowHomePiPStatusPolicyTests: XCTestCase {
)
}
func testConnectionCardDisappearsWhenSessionIsHealthy() {
XCTAssertFalse(
shouldShowConnectionCard(
lifecycle: .active,
isHostReady: true
)
)
}
func testConnectionCardRemainsVisibleWhilePreparingOrFailed() {
XCTAssertTrue(
shouldShowConnectionCard(
lifecycle: .preparing(attempt: 1, total: 3),
isHostReady: false
)
)
XCTAssertTrue(
shouldShowConnectionCard(
lifecycle: .failed(.timedOut),
isHostReady: false
)
)
}
func testNormalRecordingDoesNotBringConnectionCardBack() {
XCTAssertFalse(
shouldShowConnectionCard(
lifecycle: .active,
isRecording: true,
isHostReady: false
)
)
}
func testLocalAndPolishOnlySetupLinksToTextPolish() {
XCTAssertEqual(
HomeServiceSetupPolicy.apiKeyDeepLink(
@@ -131,13 +97,6 @@ final class FlowHomePiPStatusPolicyTests: XCTestCase {
),
.textPolish
)
XCTAssertEqual(
HomeServiceSetupPolicy.apiKeyMessageKey(
isLocalEngine: false,
isASRConfigured: true
),
"home.setup.polishKeyMissing"
)
}
func testCloudSetupLinksToSpeechRecognitionWhenASRIsMissing() {
@@ -148,13 +107,6 @@ final class FlowHomePiPStatusPolicyTests: XCTestCase {
),
.speechRecognition
)
XCTAssertEqual(
HomeServiceSetupPolicy.apiKeyMessageKey(
isLocalEngine: false,
isASRConfigured: false
),
"home.setup.cloudIncomplete"
)
}
private func descriptor(
@@ -169,20 +121,4 @@ final class FlowHomePiPStatusPolicyTests: XCTestCase {
isHostReady: false
)
}
private func shouldShowConnectionCard(
lifecycle: FlowPiPLifecycleState,
isRecording: Bool = false,
isHostReady: Bool
) -> Bool {
FlowHomePiPStatusPolicy.shouldShowConnectionCard(
lifecycle: lifecycle,
needsPermissionSetup: false,
needsAPIKeySetup: false,
hasSessionWarning: false,
isRecording: isRecording,
isProcessing: false,
isHostReady: isHostReady
)
}
}
@@ -0,0 +1,169 @@
// HomeAccountSurfacePolicyTests.swift
// OSGKeyboardTests
@testable import OSGKeyboard
import OSGKeyboardShared
import XCTest
final class HomeAccountSurfacePolicyTests: XCTestCase {
// MARK: - Readiness
func testCreditsNeedAnAccountButSurviveRestore() {
XCTAssertEqual(readiness(.managed, account: .signedIn), .credits)
XCTAssertEqual(readiness(.managed, account: .restoring), .credits)
XCTAssertEqual(readiness(.managed, account: .signedOut), .unavailable)
}
func testOwnKeyReadinessFollowsConfiguredKeys() {
XCTAssertEqual(readiness(.byok, asr: true, polish: true), .ownKey)
XCTAssertEqual(readiness(.byok, asr: true, polish: false), .dictationOnly)
XCTAssertEqual(readiness(.byok, asr: false, polish: true), .unavailable)
}
// MARK: - Surface rules, in priority order
func testMissingPermissionsOutrankEverything() {
XCTAssertEqual(
surface(needsPermissionSetup: true, account: .signedOut, pendingReferralCode: "K7M2Q9XA"),
.permissions
)
}
func testRestoringShowsNoAccountSurface() {
XCTAssertNil(surface(account: .restoring, readiness: .dictationOnly))
}
func testSavedInvitationLeadsUntilDeferred() {
XCTAssertEqual(
surface(account: .signedOut, pendingReferralCode: "K7M2Q9XA"),
.invitation(code: "K7M2Q9XA")
)
XCTAssertEqual(
surface(
account: .signedOut,
pendingReferralCode: "K7M2Q9XA",
isInvitationDeferred: true,
readiness: .dictationOnly
),
.signInSetup
)
}
func testSignedOutWithoutWorkingServiceAsksToSignIn() {
XCTAssertEqual(surface(account: .signedOut, readiness: .dictationOnly), .signInSetup)
XCTAssertEqual(surface(account: .signedOut, readiness: .unavailable), .signInSetup)
XCTAssertEqual(
surface(account: .signedOut, credentialSource: .managed, readiness: .unavailable),
.signInSetup
)
}
func testSignedOutWithWorkingKeysOnlyGetsQuietPromo() {
XCTAssertEqual(surface(account: .signedOut, readiness: .ownKey), .signInPromo)
}
func testCreditsConnectionFailureBecomesProblemCard() {
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .failed),
.creditsUnavailable
)
}
func testDepletedCreditsBecomeProblemCard() {
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .balance(0)),
.creditsDepleted
)
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .balance(-5)),
.creditsDepleted
)
}
func testHealthyOrLoadingCreditsStayQuiet() {
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .balance(1_500)),
.accountSummary
)
XCTAssertEqual(
surface(account: .signedIn, credentialSource: .managed, readiness: .credits, credits: .loading),
.accountSummary
)
}
func testSignedInWithoutKeysChoosesService() {
XCTAssertEqual(surface(account: .signedIn, readiness: .dictationOnly), .chooseService)
XCTAssertEqual(surface(account: .signedIn, readiness: .unavailable), .chooseService)
}
func testSignedInWithOwnKeysIgnoresCreditErrors() {
XCTAssertEqual(surface(account: .signedIn, readiness: .ownKey, credits: .failed), .accountSummary)
XCTAssertEqual(surface(account: .signedIn, readiness: .ownKey, credits: .balance(0)), .accountSummary)
}
func testSignedInIgnoresStalePendingInvitation() {
XCTAssertEqual(
surface(account: .signedIn, pendingReferralCode: "K7M2Q9XA", readiness: .ownKey),
.accountSummary
)
}
// MARK: - Layout
func testUnfoldedDuoUsesWideLayoutEvenWithCompactSizeClass() {
XCTAssertTrue(HomeLayoutPolicy.usesWideLayout(isRegularWidth: false, containerWidth: 669))
XCTAssertFalse(HomeLayoutPolicy.usesWideLayout(isRegularWidth: false, containerWidth: 466))
XCTAssertFalse(HomeLayoutPolicy.usesWideLayout(isRegularWidth: false, containerWidth: 402))
}
func testRegularSizeClassKeepsWideLayoutInsideSplitView() {
XCTAssertTrue(HomeLayoutPolicy.usesWideLayout(isRegularWidth: true, containerWidth: 504))
}
func testWideTopClearanceOnlyWhereNoStatusBarSitsAbove() {
// Landscape Duo inner display: status bar on the side, nothing above.
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 0), Spacing.md)
// iPad status bar, portrait Duo inner display (measured 82 pt).
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 24), 0)
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 82), 0)
// A thin inset only gets topped up to the minimum.
XCTAssertEqual(WideLayoutMetrics.topCornerClearance(topSafeAreaInset: 6), Spacing.md - 6)
}
// MARK: - Helpers
private func readiness(
_ source: CredentialSource,
account: HomeAccountPhase = .signedIn,
asr: Bool = true,
polish: Bool = true
) -> HomeServiceReadiness {
HomeAccountSurfacePolicy.readiness(
credentialSource: source,
account: account,
isASRConfigured: asr,
isPolishConfigured: polish
)
}
private func surface(
needsPermissionSetup: Bool = false,
account: HomeAccountPhase,
pendingReferralCode: String? = nil,
isInvitationDeferred: Bool = false,
credentialSource: CredentialSource = .byok,
readiness: HomeServiceReadiness = .ownKey,
credits: HomeCreditsState = .loading
) -> HomeAccountSurface? {
HomeAccountSurfacePolicy.surface(
needsPermissionSetup: needsPermissionSetup,
account: account,
pendingReferralCode: pendingReferralCode,
isInvitationDeferred: isInvitationDeferred,
credentialSource: credentialSource,
readiness: readiness,
credits: credits
)
}
}