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

Commit a5fb810

Browse files
committed
feat(example): add detail sheet
1 parent 76024df commit a5fb810

File tree

7 files changed

+353
-20
lines changed

7 files changed

+353
-20
lines changed

Example/Martie.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
C0UI000D2D0000000000000D /* TestingNotesCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100D2D0000000000000D /* TestingNotesCard.swift */; };
3535
C0UI000E2D0000000000000E /* TestingNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100E2D0000000000000E /* TestingNote.swift */; };
3636
C0UI000F2D0000000000000F /* PurchaseCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI100F2D0000000000000F /* PurchaseCard.swift */; };
37+
C0UI00102D00000000000010 /* PurchaseDetailSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0UI10102D00000000000010 /* PurchaseDetailSheet.swift */; };
3738
/* End PBXBuildFile section */
3839

3940
/* Begin PBXFileReference section */
@@ -64,6 +65,7 @@
6465
C0UI100D2D0000000000000D /* TestingNotesCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingNotesCard.swift; sourceTree = "<group>"; };
6566
C0UI100E2D0000000000000E /* TestingNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestingNote.swift; sourceTree = "<group>"; };
6667
C0UI100F2D0000000000000F /* PurchaseCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseCard.swift; sourceTree = "<group>"; };
68+
C0UI10102D00000000000010 /* PurchaseDetailSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseDetailSheet.swift; sourceTree = "<group>"; };
6769
/* End PBXFileReference section */
6870

6971
/* Begin PBXFrameworksBuildPhase section */
@@ -163,6 +165,7 @@
163165
C0UI100D2D0000000000000D /* TestingNotesCard.swift */,
164166
C0UI100E2D0000000000000E /* TestingNote.swift */,
165167
C0UI100F2D0000000000000F /* PurchaseCard.swift */,
168+
C0UI10102D00000000000010 /* PurchaseDetailSheet.swift */,
166169
);
167170
path = uis;
168171
sourceTree = "<group>";
@@ -267,6 +270,7 @@
267270
C0UI000D2D0000000000000D /* TestingNotesCard.swift in Sources */,
268271
C0UI000E2D0000000000000E /* TestingNote.swift in Sources */,
269272
C0UI000F2D0000000000000F /* PurchaseCard.swift in Sources */,
273+
C0UI00102D00000000000010 /* PurchaseDetailSheet.swift in Sources */,
270274
);
271275
runOnlyForDeploymentPostprocessing = 0;
272276
};

Example/OpenIapExample/Screens/AvailablePurchasesScreen.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ struct AvailablePurchasesScreen: View {
66
@StateObject private var iapStore = OpenIapStore()
77
@State private var showError = false
88
@State private var errorMessage = ""
9+
@State private var selectedPurchase: OpenIapPurchase?
910

1011
var body: some View {
1112
ScrollView {
@@ -33,6 +34,9 @@ struct AvailablePurchasesScreen: View {
3334
.disabled(iapStore.status.loadings.restorePurchases)
3435
}
3536
}
37+
.sheet(item: $selectedPurchase) { purchase in
38+
PurchaseDetailSheet(purchase: purchase)
39+
}
3640
.alert("Error", isPresented: $showError) {
3741
Button("OK") {}
3842
} message: {
@@ -96,11 +100,13 @@ struct AvailablePurchasesScreen: View {
96100
} else {
97101
VStack(spacing: 12) {
98102
ForEach(uniqueActivePurchases, id: \.transactionId) { purchase in
99-
ActivePurchaseCard(purchase: purchase) {
103+
ActivePurchaseCard(purchase: purchase, onConsume: {
100104
Task {
101105
await finishPurchase(purchase)
102106
}
103-
}
107+
}, onShowDetails: {
108+
selectedPurchase = purchase
109+
})
104110
}
105111
}
106112
.padding(.horizontal)
@@ -131,7 +137,9 @@ struct AvailablePurchasesScreen: View {
131137
} else {
132138
VStack(spacing: 12) {
133139
ForEach(iapStore.iosAvailablePurchases.sorted { $0.transactionDate > $1.transactionDate }, id: \.transactionId) { purchase in
134-
PurchaseHistoryCard(purchase: purchase)
140+
PurchaseHistoryCard(purchase: purchase) {
141+
selectedPurchase = purchase
142+
}
135143
}
136144
}
137145
.padding(.horizontal)
@@ -349,13 +357,13 @@ struct AvailablePurchasesScreen: View {
349357
loadPurchases()
350358
print("🧪 [AvailablePurchases] Clear transactions requested (reloaded purchases)")
351359
}
352-
360+
353361
private func syncSubscriptions() async {
354362
// Reload purchases to sync subscription status
355363
loadPurchases()
356364
print("🧪 [AvailablePurchases] Subscription sync requested (reloaded purchases)")
357365
}
358-
366+
359367
private func finishUnfinishedTransactions() async {
360368
let unfinishedPurchases = iapStore.iosAvailablePurchases.filter { !$0.purchaseState.isAcknowledged }
361369

Example/OpenIapExample/Screens/PurchaseFlowScreen.swift

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ struct PurchaseFlowScreen: View {
88
// UI State
99
@State private var showPurchaseResult = false
1010
@State private var purchaseResultMessage = ""
11+
@State private var latestPurchase: OpenIapPurchase?
12+
@State private var selectedPurchase: OpenIapPurchase?
1113
@State private var showError = false
1214
@State private var errorMessage = ""
1315

@@ -57,6 +59,9 @@ struct PurchaseFlowScreen: View {
5759
} message: {
5860
Text(errorMessage)
5961
}
62+
.sheet(item: $selectedPurchase) { purchase in
63+
PurchaseDetailSheet(purchase: purchase)
64+
}
6065
}
6166

6267
@ViewBuilder
@@ -121,17 +126,30 @@ struct PurchaseFlowScreen: View {
121126
Button("Dismiss") {
122127
showPurchaseResult = false
123128
purchaseResultMessage = ""
129+
latestPurchase = nil
124130
}
125131
.font(.caption)
126132
.foregroundColor(AppColors.primary)
127133
}
128134

129-
Text(purchaseResultMessage)
130-
.font(.system(.caption, design: .monospaced))
131-
.frame(maxWidth: .infinity, alignment: .leading)
135+
Button {
136+
if let purchase = latestPurchase {
137+
selectedPurchase = purchase
138+
}
139+
} label: {
140+
HStack(alignment: .top, spacing: 8) {
141+
Image(systemName: "chevron.right.circle.fill")
142+
.foregroundColor(AppColors.primary)
143+
Text(purchaseResultMessage)
144+
.font(.system(.caption, design: .monospaced))
145+
.foregroundColor(.primary)
146+
.frame(maxWidth: .infinity, alignment: .leading)
147+
}
132148
.padding()
133149
.background(Color.gray.opacity(0.1))
134150
.cornerRadius(8)
151+
}
152+
.buttonStyle(.plain)
135153
}
136154
.padding()
137155
.background(AppColors.cardBackground)
@@ -192,7 +210,7 @@ struct PurchaseFlowScreen: View {
192210
}
193211
}
194212
}
195-
213+
196214
iapStore.onPurchaseError = { error in
197215
Task { @MainActor in
198216
self.handlePurchaseError(error)
@@ -246,7 +264,6 @@ struct PurchaseFlowScreen: View {
246264

247265
private func purchaseProduct(_ product: OpenIapProduct) {
248266
print("🛒 [PurchaseFlow] Starting purchase for: \(product.id)")
249-
250267
Task {
251268
do {
252269
let requestType: ProductQueryType = product.type == .subs ? .subs : .inApp
@@ -272,7 +289,8 @@ struct PurchaseFlowScreen: View {
272289
Date: \(DateFormatter.localizedString(from: transactionDate, dateStyle: .short, timeStyle: .short))
273290
"""
274291
showPurchaseResult = true
275-
292+
latestPurchase = purchase
293+
276294
// In production, validate receipt on your server before finishing
277295
Task {
278296
await finishPurchase(purchase)
@@ -292,7 +310,7 @@ struct PurchaseFlowScreen: View {
292310
showError = true
293311
}
294312
}
295-
313+
296314
private func finishPurchase(_ purchase: OpenIapPurchase) async {
297315
do {
298316
try await iapStore.finishTransaction(purchase: purchase)

Example/OpenIapExample/Screens/SubscriptionFlowScreen.swift

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ struct SubscriptionFlowScreen: View {
88
// UI State
99
@State private var showError = false
1010
@State private var errorMessage = ""
11+
@State private var recentPurchase: OpenIapPurchase?
12+
@State private var selectedPurchase: OpenIapPurchase?
1113

1214
// Product IDs for subscription testing
1315
private let subscriptionIds: [String] = [
@@ -51,6 +53,10 @@ struct SubscriptionFlowScreen: View {
5153
if iapStore.status.isLoading {
5254
LoadingCard(text: "Loading subscriptions...")
5355
} else {
56+
if let purchase = recentPurchase {
57+
purchaseResultCard(for: purchase)
58+
}
59+
5460
let subscriptionProducts = iapStore.iosSubscriptionProducts
5561

5662
if subscriptionProducts.isEmpty {
@@ -173,6 +179,9 @@ struct SubscriptionFlowScreen: View {
173179
}
174180
.disabled(iapStore.status.isLoading)
175181
)
182+
.sheet(item: $selectedPurchase) { purchase in
183+
PurchaseDetailSheet(purchase: purchase)
184+
}
176185
.alert("Error", isPresented: $showError) {
177186
Button("OK") {}
178187
} message: {
@@ -273,9 +282,7 @@ struct SubscriptionFlowScreen: View {
273282
// MARK: - Purchase Flow
274283

275284
private func purchaseProduct(_ product: OpenIapSubscriptionProduct) {
276-
277285
print("🔄 [SubscriptionFlow] Starting subscription purchase for: \(product.id)")
278-
279286
Task {
280287
do {
281288
_ = try await iapStore.requestPurchase(sku: product.id, type: .subs, autoFinish: true)
@@ -320,10 +327,64 @@ struct SubscriptionFlowScreen: View {
320327
Task {
321328
await loadPurchases()
322329
}
330+
recentPurchase = purchase
323331
}
324332

325333
private func handlePurchaseError(_ error: OpenIapError) {
326334
print("❌ [SubscriptionFlow] Subscription error: \(error.message)")
327335
// Error status is already handled internally by OpenIapStore
328336
}
329337
}
338+
339+
@available(iOS 15.0, *)
340+
private extension SubscriptionFlowScreen {
341+
func purchaseResultCard(for purchase: OpenIapPurchase) -> some View {
342+
let transactionDate = Date(timeIntervalSince1970: purchase.transactionDate / 1000)
343+
let formattedDate = DateFormatter.localizedString(from: transactionDate, dateStyle: .short, timeStyle: .short)
344+
let message = """
345+
✅ Subscription successful
346+
Product: \(purchase.productId)
347+
Transaction ID: \(purchase.id)
348+
Date: \(formattedDate)
349+
"""
350+
351+
return VStack(alignment: .leading, spacing: 12) {
352+
HStack {
353+
Image(systemName: "checkmark.circle.fill")
354+
.foregroundColor(AppColors.success)
355+
Text("Latest Subscription")
356+
.font(.headline)
357+
358+
Spacer()
359+
360+
Button("Dismiss") {
361+
recentPurchase = nil
362+
}
363+
.font(.caption)
364+
.foregroundColor(AppColors.primary)
365+
}
366+
367+
Button {
368+
selectedPurchase = purchase
369+
} label: {
370+
HStack(alignment: .top, spacing: 8) {
371+
Image(systemName: "chevron.right.circle.fill")
372+
.foregroundColor(AppColors.primary)
373+
Text(message)
374+
.font(.system(.caption, design: .monospaced))
375+
.foregroundColor(.primary)
376+
.frame(maxWidth: .infinity, alignment: .leading)
377+
}
378+
.padding()
379+
.background(Color.gray.opacity(0.1))
380+
.cornerRadius(8)
381+
}
382+
.buttonStyle(.plain)
383+
}
384+
.padding()
385+
.background(AppColors.cardBackground)
386+
.cornerRadius(12)
387+
.shadow(radius: 2)
388+
.padding(.horizontal)
389+
}
390+
}

Example/OpenIapExample/Screens/uis/ActivePurchaseCard.swift

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import OpenIAP
44
struct ActivePurchaseCard: View {
55
let purchase: OpenIapPurchase
66
let onConsume: () -> Void
7+
let onShowDetails: () -> Void
78

89
private var isSubscription: Bool {
9-
purchase.productId.contains("premium")
10+
purchase.isSubscription
1011
}
1112

1213
var body: some View {
@@ -18,11 +19,19 @@ struct ActivePurchaseCard: View {
1819
.background((isSubscription ? AppColors.warning : AppColors.success).opacity(0.1))
1920
.cornerRadius(12)
2021

21-
VStack(alignment: .leading, spacing: 4) {
22-
Text(purchase.id)
22+
VStack(alignment: .leading, spacing: 6) {
23+
Text(purchase.productId)
2324
.font(.headline)
2425
.fontWeight(.semibold)
25-
26+
27+
Text("Transaction: \(purchase.id)")
28+
.font(.caption)
29+
.foregroundColor(.secondary)
30+
31+
Text(Date(timeIntervalSince1970: purchase.transactionDate / 1000), style: .date)
32+
.font(.caption)
33+
.foregroundColor(.secondary)
34+
2635
if isSubscription && purchase.isAutoRenewing {
2736
Label("Auto-renewable", systemImage: "arrow.triangle.2.circlepath")
2837
.font(.caption)
@@ -37,6 +46,13 @@ struct ActivePurchaseCard: View {
3746
}
3847

3948
Spacer()
49+
50+
Button(action: onShowDetails) {
51+
Image(systemName: "info.circle")
52+
.font(.system(size: 18))
53+
.foregroundColor(AppColors.primary)
54+
}
55+
.buttonStyle(.plain)
4056

4157
if !isSubscription && !purchase.purchaseState.isAcknowledged {
4258
Button(action: onConsume) {
@@ -61,4 +77,3 @@ struct ActivePurchaseCard: View {
6177
.padding(.horizontal)
6278
}
6379
}
64-

0 commit comments

Comments
 (0)