Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.

Commit 800793a

Browse files
authored
Restore legacy compatibility APIs (#11)
## Summary - expose legacy error helpers and typealiases so older integrations stay compatible - add serialization wrappers mirroring historic API responses for products and purchases - correctly implement promoted product - Improve purchase handling and add serialization helpers
1 parent 275305a commit 800793a

File tree

11 files changed

+654
-159
lines changed

11 files changed

+654
-159
lines changed

Example/OpenIapExample/Models/IapCompat.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ extension PurchaseState {
2525
@available(iOS 15.0, *)
2626
extension PurchaseIOS {
2727
var isSubscription: Bool {
28-
expirationDateIOS != nil || isAutoRenewing
28+
if expirationDateIOS != nil { return true }
29+
if isAutoRenewing { return true }
30+
// Newly purchased subscriptions can report neither expiration nor auto-renew yet,
31+
// but StoreKit always adds the subscription group identifier for them.
32+
if let groupId = subscriptionGroupIdIOS, groupId.isEmpty == false { return true }
33+
return false
2934
}
3035
}
3136

Example/OpenIapExample/Screens/AvailablePurchasesScreen.swift

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,22 @@ struct AvailablePurchasesScreen: View {
77
@State private var showError = false
88
@State private var errorMessage = ""
99
@State private var selectedPurchase: OpenIapPurchase?
10+
@State private var isInitialLoading = true
1011

1112
var body: some View {
1213
ScrollView {
1314
VStack(spacing: 24) {
14-
availablePurchasesSection
15-
purchaseHistorySection
16-
17-
// Debug section for Sandbox testing
18-
#if DEBUG
19-
sandboxToolsSection
20-
#endif
15+
if isInitialLoading {
16+
LoadingCard(text: "Loading purchases...")
17+
} else {
18+
availablePurchasesSection
19+
purchaseHistorySection
20+
21+
// Debug section for Sandbox testing
22+
#if DEBUG
23+
sandboxToolsSection
24+
#endif
25+
}
2126
}
2227
.padding(.vertical)
2328
}
@@ -26,9 +31,9 @@ struct AvailablePurchasesScreen: View {
2631
.navigationBarTitleDisplayMode(.large)
2732
.toolbar {
2833
ToolbarItem(placement: .navigationBarTrailing) {
29-
Button(action: {
30-
loadPurchases()
31-
}) {
34+
Button {
35+
Task { await loadPurchases() }
36+
} label: {
3237
Image(systemName: "arrow.clockwise")
3338
}
3439
.disabled(iapStore.status.loadings.restorePurchases)
@@ -48,11 +53,17 @@ struct AvailablePurchasesScreen: View {
4853
Text(errorMessage)
4954
}
5055
.onAppear {
56+
isInitialLoading = true
5157
setupIapProvider()
5258
}
5359
.onDisappear {
5460
teardownConnection()
5561
}
62+
.onChange(of: iapStore.iosAvailablePurchases.count) { _ in
63+
if isInitialLoading {
64+
isInitialLoading = false
65+
}
66+
}
5667
}
5768

5869
// MARK: - Available Purchases Section (Currently Owned/Active)
@@ -76,20 +87,23 @@ struct AvailablePurchasesScreen: View {
7687
if purchase.isSubscription {
7788
// Active subscriptions: check auto-renewing or expiry time
7889
if purchase.isAutoRenewing {
90+
OpenIapLog.debug("📦 Active subscription auto-renewing product=\(purchase.productId)")
7991
return true // Always show auto-renewing subscriptions
8092
}
8193
// For non-auto-renewing, check expiry time
8294
if let expiryTime = purchase.expirationDateIOS {
8395
let expiryDate = Date(timeIntervalSince1970: expiryTime / 1000)
96+
OpenIapLog.debug("📦 Subscription product=\(purchase.productId) expiry=\(expiryDate) isActive=\(expiryDate > Date())")
8497
return expiryDate > Date() // Only show if not expired
8598
}
8699
return true // Show if no expiry info
87100
} else {
88101
// Consumables: show if not acknowledged
102+
OpenIapLog.debug("📦 Consumable product=\(purchase.productId) state=\(purchase.purchaseState.rawValue) isAcknowledged=\(purchase.purchaseState.isAcknowledged)")
89103
return !purchase.purchaseState.isAcknowledged
90104
}
91105
}
92-
106+
93107
// Return sorted by date
94108
return allActivePurchases.sorted(by: { $0.transactionDate > $1.transactionDate })
95109
}
@@ -121,6 +135,26 @@ struct AvailablePurchasesScreen: View {
121135
// MARK: - Purchase History Section (All Past Purchases)
122136
private var purchaseHistorySection: some View {
123137
VStack(alignment: .leading, spacing: 16) {
138+
Button {
139+
Task { await showManageSubscriptions() }
140+
} label: {
141+
HStack {
142+
Image(systemName: "gearshape.fill")
143+
Text("Manage Subscriptions")
144+
.fontWeight(.semibold)
145+
Spacer()
146+
Image(systemName: "arrow.up.forward.app")
147+
.font(.caption)
148+
}
149+
.padding()
150+
.frame(maxWidth: .infinity)
151+
.background(AppColors.secondary)
152+
.foregroundColor(.white)
153+
.cornerRadius(10)
154+
}
155+
.buttonStyle(.plain)
156+
.padding(.horizontal)
157+
124158
SectionHeaderView(
125159
title: "Purchase History",
126160
subtitle: "All your past purchases",
@@ -287,30 +321,30 @@ struct AvailablePurchasesScreen: View {
287321

288322
iapStore.onPurchaseSuccess = { purchase in
289323
if purchase.asIOS() != nil {
290-
Task { @MainActor in
291-
loadPurchases()
292-
}
324+
Task { await loadPurchases() }
293325
}
294326
}
295-
327+
296328
iapStore.onPurchaseError = { error in
297329
Task { @MainActor in
298330
errorMessage = error.message
299331
showError = true
300332
}
301333
}
302-
334+
303335
Task {
304336
do {
305337
try await iapStore.initConnection()
306338
print("✅ [AvailablePurchases] Connection initialized")
307-
loadPurchases()
339+
await loadPurchases()
308340
} catch {
309341
await MainActor.run {
310342
errorMessage = "Failed to initialize connection: \(error.localizedDescription)"
311343
showError = true
344+
isInitialLoading = false
312345
}
313346
}
347+
await MainActor.run { isInitialLoading = false }
314348
}
315349
}
316350

@@ -324,16 +358,32 @@ struct AvailablePurchasesScreen: View {
324358

325359
// MARK: - Purchase Loading
326360

327-
private func loadPurchases() {
328-
Task {
329-
do {
330-
try await iapStore.getAvailablePurchases()
331-
print("✅ [AvailablePurchases] Loaded \(iapStore.iosAvailablePurchases.count) purchases")
332-
} catch {
333-
await MainActor.run {
334-
errorMessage = "Failed to load purchases: \(error.localizedDescription)"
335-
showError = true
336-
}
361+
@MainActor
362+
private func loadPurchases(retry: Int = 0) async {
363+
isInitialLoading = false
364+
do {
365+
try await iapStore.getAvailablePurchases()
366+
print("✅ [AvailablePurchases] Loaded \(iapStore.iosAvailablePurchases.count) purchases")
367+
} catch {
368+
errorMessage = "Failed to load purchases: \(error.localizedDescription)"
369+
showError = true
370+
}
371+
372+
if iapStore.iosAvailablePurchases.isEmpty && retry < 3 {
373+
let delay = UInt64((retry + 1) * 2) * 1_000_000_000
374+
OpenIapLog.debug("🕑 No purchases yet, retrying loadPurchases attempt=\(retry + 1)")
375+
try? await Task.sleep(nanoseconds: delay)
376+
await loadPurchases(retry: retry + 1)
377+
}
378+
}
379+
380+
private func showManageSubscriptions() async {
381+
do {
382+
_ = try await iapStore.showManageSubscriptionsIOS()
383+
} catch {
384+
await MainActor.run {
385+
errorMessage = "Failed to open subscription management: \(error.localizedDescription)"
386+
showError = true
337387
}
338388
}
339389
}
@@ -344,8 +394,7 @@ struct AvailablePurchasesScreen: View {
344394
do {
345395
try await iapStore.finishTransaction(purchase: purchase)
346396
print("✅ [AvailablePurchases] Transaction finished: \(purchase.id)")
347-
// Reload purchases to update UI
348-
loadPurchases()
397+
await loadPurchases()
349398
} catch {
350399
await MainActor.run {
351400
errorMessage = "Failed to finish transaction: \(error.localizedDescription)"
@@ -359,13 +408,13 @@ struct AvailablePurchasesScreen: View {
359408
private func clearAllTransactions() async {
360409
// Note: This would require additional API in OpenIapStore
361410
// For now, just reload purchases
362-
loadPurchases()
411+
await loadPurchases()
363412
print("🧪 [AvailablePurchases] Clear transactions requested (reloaded purchases)")
364413
}
365-
414+
366415
private func syncSubscriptions() async {
367416
// Reload purchases to sync subscription status
368-
loadPurchases()
417+
await loadPurchases()
369418
print("🧪 [AvailablePurchases] Subscription sync requested (reloaded purchases)")
370419
}
371420

@@ -382,7 +431,7 @@ struct AvailablePurchasesScreen: View {
382431
}
383432

384433
// Reload after finishing transactions
385-
loadPurchases()
434+
await loadPurchases()
386435
}
387436
}
388437

Example/OpenIapExample/Screens/PurchaseFlowScreen.swift

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ struct PurchaseFlowScreen: View {
1212
@State private var selectedPurchase: OpenIapPurchase?
1313
@State private var showError = false
1414
@State private var errorMessage = ""
15+
@State private var isInitialLoading = true
1516

1617
// Product IDs configured in App Store Connect
1718
private let productIds: [String] = [
@@ -24,11 +25,15 @@ struct PurchaseFlowScreen: View {
2425
ScrollView(.vertical, showsIndicators: true) {
2526
VStack(spacing: 20) {
2627
HeaderCardView()
27-
28-
ProductsSection()
29-
30-
if showPurchaseResult {
31-
PurchaseResultSection()
28+
29+
if isInitialLoading {
30+
LoadingCard(text: "Loading products...")
31+
} else {
32+
ProductsSection()
33+
34+
if showPurchaseResult {
35+
PurchaseResultSection()
36+
}
3237
}
3338

3439
InstructionsCard()
@@ -42,17 +47,24 @@ struct PurchaseFlowScreen: View {
4247
.navigationBarTitleDisplayMode(.large)
4348
.toolbar {
4449
ToolbarItem(placement: .navigationBarTrailing) {
45-
Button(action: loadProducts) {
50+
Button {
51+
Task { await loadProducts() }
52+
} label: {
4653
Image(systemName: "arrow.clockwise")
4754
}
48-
.disabled(iapStore.status.isLoading)
55+
.disabled(isInitialLoading || iapStore.status.isLoading)
4956
}
5057
}
5158
.onAppear {
59+
isInitialLoading = true
5260
setupIapProvider()
5361
}
5462
.onDisappear {
63+
iapStore.resetEphemeralState()
5564
teardownConnection()
65+
selectedPurchase = nil
66+
latestPurchase = nil
67+
showPurchaseResult = false
5668
}
5769
.alert("Error", isPresented: $showError) {
5870
Button("OK") { }
@@ -226,13 +238,15 @@ struct PurchaseFlowScreen: View {
226238
do {
227239
try await iapStore.initConnection()
228240
print("✅ [PurchaseFlow] Connection initialized")
229-
loadProducts()
241+
await loadProducts()
230242
} catch {
231243
await MainActor.run {
232244
errorMessage = "Failed to initialize connection: \(error.localizedDescription)"
233245
showError = true
234246
}
235247
}
248+
249+
await MainActor.run { isInitialLoading = false }
236250
}
237251
}
238252

@@ -246,22 +260,20 @@ struct PurchaseFlowScreen: View {
246260

247261
// MARK: - Product Loading
248262

249-
private func loadProducts() {
250-
Task {
251-
do {
252-
try await iapStore.fetchProducts(skus: productIds, type: .inApp)
253-
await MainActor.run {
254-
if iapStore.iosProducts.isEmpty {
255-
errorMessage = "No products found. Please check your App Store Connect configuration."
256-
showError = true
257-
}
258-
}
259-
} catch {
260-
await MainActor.run {
261-
errorMessage = "Failed to load products: \(error.localizedDescription)"
263+
private func loadProducts() async {
264+
do {
265+
try await iapStore.fetchProducts(skus: productIds, type: .inApp)
266+
await MainActor.run {
267+
if iapStore.iosProducts.isEmpty {
268+
errorMessage = "No products found. Please check your App Store Connect configuration."
262269
showError = true
263270
}
264271
}
272+
} catch {
273+
await MainActor.run {
274+
errorMessage = "Failed to load products: \(error.localizedDescription)"
275+
showError = true
276+
}
265277
}
266278
}
267279

0 commit comments

Comments
 (0)