Skip to content

Commit ccf2bfc

Browse files
committed
Retry in-app verification 3 times before failing
StoreKit is unable to report failures and may occasionally return empty products instead. This is a mitigation attempt to avoid "Purchase required" for legit paying users.
1 parent c8477c5 commit ccf2bfc

File tree

3 files changed

+34
-7
lines changed

3 files changed

+34
-7
lines changed

Packages/App/Sources/CommonLibrary/Domain/Constants.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ public struct Constants: Decodable, Sendable {
120120
public let delay: TimeInterval
121121

122122
public let interval: TimeInterval
123+
124+
public let attempts: Int
125+
126+
public let retryInterval: TimeInterval
123127
}
124128

125129
public let production: Parameters

Packages/App/Sources/CommonLibrary/Resources/Constants.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,15 @@
3838
"verification": {
3939
"production": {
4040
"delay": 120.0,
41-
"interval": 3600.0
41+
"interval": 3600.0,
42+
"attempts": 3,
43+
"retryInterval": 20.0
4244
},
4345
"beta": {
4446
"delay": 600.0,
45-
"interval": 600.0
47+
"interval": 600.0,
48+
"attempts": 3,
49+
"retryInterval": 10.0
4650
}
4751
}
4852
},

Passepartout/Tunnel/PacketTunnelProvider.swift

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
8989
await verifyEligibility(
9090
of: fwd.originalProfile,
9191
environment: environment,
92-
interval: params.interval
92+
params: params
9393
)
9494
}
9595
} catch {
@@ -127,13 +127,32 @@ final class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable {
127127
// MARK: - Eligibility
128128

129129
private extension PacketTunnelProvider {
130-
func verifyEligibility(of profile: Profile, environment: TunnelEnvironment, interval: TimeInterval) async {
130+
131+
@MainActor
132+
func verifyEligibility(
133+
of profile: Profile,
134+
environment: TunnelEnvironment,
135+
params: Constants.Tunnel.Verification.Parameters
136+
) async {
137+
var attempts = params.attempts
138+
131139
while true {
132140
do {
133141
pp_log(.app, .info, "Verify profile, requires: \(profile.features)")
134142
await context.iapManager.reloadReceipt()
135-
try await context.iapManager.verify(profile)
143+
try context.iapManager.verify(profile)
136144
} catch {
145+
146+
// mitigate the StoreKit inability to report errors, sometimes it
147+
// would just return empty products, e.g. on network failure. in those
148+
// cases, retry a few times before failing
149+
if attempts > 0 {
150+
attempts -= 1
151+
pp_log(.app, .error, "Verification failed for profile \(profile.id), next attempt in \(params.retryInterval) seconds... (remaining: \(attempts), products: \(context.iapManager.purchasedProducts))")
152+
try? await Task.sleep(interval: params.retryInterval)
153+
continue
154+
}
155+
137156
let error = PartoutError(.App.ineligibleProfile)
138157
environment.setEnvironmentValue(error.code, forKey: TunnelEnvironmentKeys.lastErrorCode)
139158
pp_log(.app, .fault, "Verification failed for profile \(profile.id), shutting down: \(error)")
@@ -144,8 +163,8 @@ private extension PacketTunnelProvider {
144163
return
145164
}
146165

147-
pp_log(.app, .info, "Will verify profile again in \(interval) seconds...")
148-
try? await Task.sleep(interval: interval)
166+
pp_log(.app, .info, "Will verify profile again in \(params.interval) seconds...")
167+
try? await Task.sleep(interval: params.interval)
149168
}
150169
}
151170
}

0 commit comments

Comments
 (0)