fix(account): load permanent referral profiles
CI / Validate manifests (push) Has been cancelled
CI / SwiftLint (push) Has been cancelled
CI / iOS / Extension (push) Has been cancelled
CI / macOS (push) Has been cancelled

Use authenticated server invitation URLs with account-scoped caching and bounded state transitions, then advance the iOS build metadata to 86.
This commit is contained in:
Rocky
2026-08-20 22:59:58 +08:00
parent ca0a1f9b9a
commit 7bd77509b6
19 changed files with 1151 additions and 228 deletions
@@ -38,7 +38,11 @@ enum LiveAccountDependencyFactory {
grantStore: grantStore,
configuration: AppGroupStore()
)
return AccountDependencies(sessionService: service, centerService: service)
return AccountDependencies(
sessionService: service,
centerService: service,
referralService: LiveReferralProfileService(apiClient: apiClient)
)
}
}
@@ -55,21 +59,11 @@ actor AccountCenterSnapshotLoader {
) async throws -> AccountCenterSnapshot {
async let account = apiClient.account()
async let balance = resource(CreditBalanceDTO.self, .creditsBalance)
async let profile = optionalResource(
ReferralProfileDTO.self,
.referralProfile,
diagnosticName: "referral-profile"
)
async let referrals = optionalResource(
[ReferralBindingDTO].self,
.referrals(limit: 50),
diagnosticName: "referrals"
)
async let campaigns = optionalResource(
[ReferralCampaignDTO].self,
.referralCampaigns,
diagnosticName: "referral-campaigns"
)
let core: (OSGAccount, CreditBalanceDTO)
do {
@@ -81,28 +75,7 @@ actor AccountCenterSnapshotLoader {
)
throw error
}
let optionalValues = await (profile, referrals, campaigns)
let invitationCode = await resolvedInvitationCode(
profile: optionalValues.0,
cachedProfile: cachedSnapshot?.referralProfile
)
let campaign = optionalValues.2?.first {
$0.id == optionalValues.0?.code?.campaignId
} ?? optionalValues.2?.first
let referralProfile = if optionalValues.0 == nil,
let cachedProfile = cachedSnapshot?.referralProfile {
cachedProfile
} else {
AccountReferralProfile(
code: invitationCode,
boundCode: cachedSnapshot?.referralProfile.boundCode,
inviterRewardCredits: campaign?.inviterRewardCredits
?? cachedSnapshot?.referralProfile.inviterRewardCredits,
inviteeRewardCredits: campaign?.inviteeRewardCredits
?? cachedSnapshot?.referralProfile.inviteeRewardCredits
)
}
let loadedReferrals = optionalValues.1?.map {
let loadedReferrals = await referrals?.map {
AccountReferral(
id: UUID(),
status: Self.referralStatus($0.rewardStatus),
@@ -121,7 +94,6 @@ actor AccountCenterSnapshotLoader {
balance: core.1.balance,
usedCredits: core.1.lifetimeUsed ?? 0
),
referralProfile: referralProfile,
referrals: loadedReferrals
)
}
@@ -151,26 +123,6 @@ actor AccountCenterSnapshotLoader {
}
}
private func resolvedInvitationCode(
profile: ReferralProfileDTO?,
cachedProfile: AccountReferralProfile?
) async -> String? {
if let code = profile?.code?.code ?? cachedProfile?.code {
return code
}
do {
let data = try await apiClient.authorizedResourceData(.createReferralCode)
return try decoder.decode(ReferralCodeDTO.self, from: data).code
} catch {
OSGDiag.log(
"account refresh optional=referral-code fallback=cache "
+ "error=\(AccountDiagnostic.code(for: error))",
category: "account"
)
return cachedProfile?.code
}
}
private static func referralStatus(_ value: String) -> AccountReferralStatus {
switch value.uppercased() {
case "REWARDED":
@@ -192,6 +144,24 @@ actor AccountCenterSnapshotLoader {
}
}
actor LiveReferralProfileService: ReferralProfileServicing {
private let apiClient: AccountAPIClient
private let decoder = JSONDecoder()
init(apiClient: AccountAPIClient) {
self.apiClient = apiClient
}
func loadReferralProfile() async throws -> ReferralProfile {
let data = try await apiClient.authorizedResourceData(.referralProfile)
do {
return try decoder.decode(ReferralProfile.self, from: data)
} catch {
throw AccountAPIError.decoding
}
}
}
private actor LiveAccountService: AccountSessionServicing, AccountCenterServicing {
private let apiClient: AccountAPIClient
private let accountCenterLoader: AccountCenterSnapshotLoader
@@ -356,11 +326,6 @@ private actor LiveAccountService: AccountSessionServicing, AccountCenterServicin
)
}
func createReferralCode() async throws -> String {
let data = try await apiClient.authorizedResourceData(.createReferralCode)
return try decoder.decode(ReferralCodeDTO.self, from: data).code
}
func redeemReferral(code: String) async throws {
struct Request: Encodable { let code: String }
let body = try encoder.encode(Request(code: code))
@@ -586,21 +551,6 @@ private struct CreditBalanceDTO: Decodable, Sendable {
let lifetimeUsed: Int64?
}
private struct ReferralCodeDTO: Decodable, Sendable {
let code: String
let campaignId: String?
}
private struct ReferralProfileDTO: Decodable, Sendable {
let code: ReferralCodeDTO?
}
private struct ReferralCampaignDTO: Decodable, Sendable {
let id: String
let inviterRewardCredits: Int64
let inviteeRewardCredits: Int64
}
private struct ReferralBindingDTO: Decodable, Sendable {
let boundAt: String
let rewardStatus: String