@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/docs/facts.json +6 -6
- package/manifest.json +55 -34
- package/package.json +1 -1
- package/pipeline/multi-agent-refs/features/usage-reporting.md +7 -3
- package/pipeline/schemas/prefs.schema.json +4 -0
- package/pipeline/scripts/usage-register.mjs +2 -0
- package/pipeline/skills/.skill-manifest.json +36 -20
- package/pipeline/skills/.skills-index.json +75 -9
- package/pipeline/skills/shared/README.md +13 -7
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
- package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
- package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
- package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
- package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
- package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
- package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
- package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
- package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
- package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
- package/pipeline/skills/skills-index.md +8 -2
|
@@ -16,6 +16,10 @@ When reviewing StoreKit code, explicitly separate "preferred SwiftUI path" from
|
|
|
16
16
|
direct `product.purchase(options:)` is still valid for lower-level custom
|
|
17
17
|
StoreKit flows.
|
|
18
18
|
|
|
19
|
+
Full, copyable code for the core flows lives in
|
|
20
|
+
[references/core-patterns.md](references/core-patterns.md). This guide keeps the
|
|
21
|
+
review guidance and short snippets; load the reference for a complete example.
|
|
22
|
+
|
|
19
23
|
## Contents
|
|
20
24
|
|
|
21
25
|
- [Implementation Review Minimums](#implementation-review-minimums)
|
|
@@ -24,11 +28,10 @@ StoreKit flows.
|
|
|
24
28
|
- [Purchase Flow](#purchase-flow)
|
|
25
29
|
- [Transaction.updates Listener](#transactionupdates-listener)
|
|
26
30
|
- [Entitlement Checking](#entitlement-checking)
|
|
27
|
-
- [
|
|
28
|
-
- [StoreView (iOS 17+)](#storeview-ios-17)
|
|
31
|
+
- [Store Views (SubscriptionStoreView, StoreView, ProductView)](#store-views-subscriptionstoreview-storeview-productview)
|
|
29
32
|
- [Subscription Status Checking](#subscription-status-checking)
|
|
30
33
|
- [Restore Purchases](#restore-purchases)
|
|
31
|
-
- [App Transaction
|
|
34
|
+
- [App Transaction](#app-transaction)
|
|
32
35
|
- [Purchase Options](#purchase-options)
|
|
33
36
|
- [SwiftUI Purchase Callbacks](#swiftui-purchase-callbacks)
|
|
34
37
|
- [Common Mistakes](#common-mistakes)
|
|
@@ -76,20 +79,13 @@ points explicitly:
|
|
|
76
79
|
Define product IDs as constants. Fetch products with `Product.products(for:)`.
|
|
77
80
|
|
|
78
81
|
```swift
|
|
79
|
-
import StoreKit
|
|
80
|
-
|
|
81
82
|
enum ProductID {
|
|
82
83
|
static let premium = "com.myapp.premium"
|
|
83
|
-
static let gems100 = "com.myapp.gems100"
|
|
84
84
|
static let monthlyPlan = "com.myapp.monthly"
|
|
85
|
-
static let
|
|
86
|
-
static let all: [String] = [premium, gems100, monthlyPlan, yearlyPlan]
|
|
85
|
+
static let all: [String] = [premium, monthlyPlan]
|
|
87
86
|
}
|
|
88
87
|
|
|
89
88
|
let products = try await Product.products(for: ProductID.all)
|
|
90
|
-
for product in products {
|
|
91
|
-
print("\(product.displayName): \(product.displayPrice)")
|
|
92
|
-
}
|
|
93
89
|
```
|
|
94
90
|
|
|
95
91
|
## Purchase Flow
|
|
@@ -105,35 +101,8 @@ Review wording: do not call `product.purchase(options:)` inherently wrong. Say
|
|
|
105
101
|
"prefer `PurchaseAction` for SwiftUI buttons; keep `product.purchase(options:)`
|
|
106
102
|
for lower-level custom flows that need direct StoreKit control."
|
|
107
103
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
func purchaseProduct(_ product: Product) async throws {
|
|
112
|
-
let result = try await purchase(product, options: [
|
|
113
|
-
.appAccountToken(userAccountToken)
|
|
114
|
-
])
|
|
115
|
-
switch result {
|
|
116
|
-
case .success(let verification):
|
|
117
|
-
let transaction = try checkVerified(verification)
|
|
118
|
-
await deliverContent(for: transaction)
|
|
119
|
-
await transaction.finish()
|
|
120
|
-
case .userCancelled:
|
|
121
|
-
break
|
|
122
|
-
case .pending:
|
|
123
|
-
// Ask to Buy or deferred approval: show pending UI, no unlock yet.
|
|
124
|
-
showPendingApprovalMessage()
|
|
125
|
-
@unknown default:
|
|
126
|
-
break
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
|
|
131
|
-
switch result {
|
|
132
|
-
case .verified(let value): return value
|
|
133
|
-
case .unverified(_, let error): throw error
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
```
|
|
104
|
+
Full purchase-flow example with `checkVerified` and `.pending` handling:
|
|
105
|
+
references/core-patterns.md#purchase-flow
|
|
137
106
|
|
|
138
107
|
## Transaction.updates Listener
|
|
139
108
|
|
|
@@ -146,30 +115,8 @@ In implementation reviews, name the launch-time coverage explicitly: purchases
|
|
|
146
115
|
made on other devices, Family Sharing changes, subscription renewals, Ask to Buy
|
|
147
116
|
approvals, refunds, revocations, and unfinished transactions.
|
|
148
117
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
struct MyApp: App {
|
|
152
|
-
private let transactionListener: Task<Void, Never>
|
|
153
|
-
|
|
154
|
-
init() {
|
|
155
|
-
transactionListener = Self.listenForTransactions()
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
var body: some Scene {
|
|
159
|
-
WindowGroup { ContentView() }
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
static func listenForTransactions() -> Task<Void, Never> {
|
|
163
|
-
Task(priority: .background) {
|
|
164
|
-
for await result in Transaction.updates {
|
|
165
|
-
guard case .verified(let transaction) = result else { continue }
|
|
166
|
-
await StoreManager.shared.updateEntitlements()
|
|
167
|
-
await transaction.finish()
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
```
|
|
118
|
+
Full launch-time listener wired into `App.init`:
|
|
119
|
+
references/core-patterns.md#transactionupdates-listener
|
|
173
120
|
|
|
174
121
|
## Entitlement Checking
|
|
175
122
|
|
|
@@ -185,144 +132,35 @@ covers non-consumables, active or grace-period auto-renewable subscriptions, and
|
|
|
185
132
|
non-renewing subscriptions; it does not include consumable purchase or delivery
|
|
186
133
|
history." Do not replace this with only a code sample or a revocation check.
|
|
187
134
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
@MainActor
|
|
191
|
-
class StoreManager {
|
|
192
|
-
static let shared = StoreManager()
|
|
193
|
-
var purchasedProductIDs: Set<String> = []
|
|
194
|
-
var isPremium: Bool { purchasedProductIDs.contains(ProductID.premium) }
|
|
195
|
-
|
|
196
|
-
func updateEntitlements() async {
|
|
197
|
-
var purchased = Set<String>()
|
|
198
|
-
for await result in Transaction.currentEntitlements {
|
|
199
|
-
if case .verified(let transaction) = result,
|
|
200
|
-
transaction.revocationDate == nil {
|
|
201
|
-
purchased.insert(transaction.productID)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
purchasedProductIDs = purchased
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
```
|
|
135
|
+
Full `StoreManager.updateEntitlements()` and the `.currentEntitlementTask`
|
|
136
|
+
SwiftUI gate: references/core-patterns.md#entitlement-checking
|
|
208
137
|
|
|
209
|
-
|
|
138
|
+
## Store Views (SubscriptionStoreView, StoreView, ProductView)
|
|
210
139
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
Group {
|
|
217
|
-
switch state {
|
|
218
|
-
case .loading: ProgressView()
|
|
219
|
-
case .failure: PaywallView()
|
|
220
|
-
case .success(.some(.verified(let transaction))) where transaction.revocationDate == nil:
|
|
221
|
-
PremiumContentView()
|
|
222
|
-
case .success:
|
|
223
|
-
PaywallView()
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
.currentEntitlementTask(for: ProductID.premium) { state in
|
|
227
|
-
self.state = state
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
|
-
## SubscriptionStoreView (iOS 17+)
|
|
234
|
-
|
|
235
|
-
Built-in SwiftUI view for subscription paywalls. Handles product loading,
|
|
236
|
-
purchase UI, and restore purchases automatically.
|
|
140
|
+
`SubscriptionStoreView(groupID:)` is the built-in subscription paywall; it loads
|
|
141
|
+
products, runs the purchase UI, and can expose restore and policy controls.
|
|
142
|
+
`StoreView(ids:)` merchandises multiple products, and `ProductView(id:)` renders
|
|
143
|
+
a single product. All expose `.onInAppPurchaseCompletion` and
|
|
144
|
+
`.storeButton(.visible, for: .restorePurchases)`.
|
|
237
145
|
|
|
238
146
|
```swift
|
|
239
147
|
SubscriptionStoreView(groupID: "YOUR_GROUP_ID")
|
|
240
|
-
.subscriptionStoreControlStyle(.prominentPicker)
|
|
241
|
-
.subscriptionStoreButtonLabel(.multiline)
|
|
242
148
|
.storeButton(.visible, for: .restorePurchases)
|
|
243
|
-
.storeButton(.visible, for: .redeemCode)
|
|
244
149
|
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
|
|
245
150
|
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
|
|
246
|
-
.onInAppPurchaseCompletion { product, result in
|
|
247
|
-
if case .success(.success(.verified(let transaction))) = result {
|
|
248
|
-
await deliverContent(for: transaction)
|
|
249
|
-
await transaction.finish()
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
### Custom Marketing Content
|
|
255
|
-
|
|
256
|
-
```swift
|
|
257
|
-
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
|
|
258
|
-
VStack {
|
|
259
|
-
Image(systemName: "crown.fill").font(.system(size: 60)).foregroundStyle(.yellow)
|
|
260
|
-
Text("Unlock Premium").font(.largeTitle.bold())
|
|
261
|
-
Text("Access all features").foregroundStyle(.secondary)
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
.containerBackground(.blue.gradient, for: .subscriptionStore)
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
### Hierarchical Layout
|
|
268
|
-
|
|
269
|
-
`SubscriptionOptionGroup`, `SubscriptionOptionSection`, and
|
|
270
|
-
`SubscriptionPeriodGroupSet` are iOS 18+ helper views for organizing options
|
|
271
|
-
inside `SubscriptionStoreView`.
|
|
272
|
-
|
|
273
|
-
```swift
|
|
274
|
-
SubscriptionStoreView(groupID: "YOUR_GROUP_ID") {
|
|
275
|
-
SubscriptionPeriodGroupSet()
|
|
276
|
-
}
|
|
277
|
-
.subscriptionStoreControlStyle(.picker)
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
## StoreView (iOS 17+)
|
|
281
|
-
|
|
282
|
-
Merchandises multiple products with localized names, prices, and purchase buttons.
|
|
283
|
-
|
|
284
|
-
```swift
|
|
285
|
-
StoreView(ids: [ProductID.gems100, ProductID.premium], prefersPromotionalIcon: true)
|
|
286
|
-
.productViewStyle(.large)
|
|
287
|
-
.storeButton(.visible, for: .restorePurchases)
|
|
288
|
-
.onInAppPurchaseCompletion { product, result in
|
|
289
|
-
if case .success(.success(.verified(let transaction))) = result {
|
|
290
|
-
await deliverContent(for: transaction)
|
|
291
|
-
await transaction.finish()
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
151
|
```
|
|
295
152
|
|
|
296
|
-
|
|
153
|
+
Full paywall, custom marketing content, `StoreView`, and `ProductView` examples:
|
|
154
|
+
references/core-patterns.md#subscriptionstoreview and
|
|
155
|
+
references/core-patterns.md#storeview-and-productview
|
|
297
156
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
switch iconPhase {
|
|
301
|
-
case .success(let image): image.resizable().scaledToFit()
|
|
302
|
-
case .loading: ProgressView()
|
|
303
|
-
default: Image(systemName: "star.fill")
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
.productViewStyle(.large)
|
|
307
|
-
```
|
|
157
|
+
For control styles, hierarchical option layouts, offers, and container
|
|
158
|
+
backgrounds see [references/storekit-advanced.md](references/storekit-advanced.md).
|
|
308
159
|
|
|
309
160
|
## Subscription Status Checking
|
|
310
161
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
|
|
314
|
-
for status in statuses {
|
|
315
|
-
guard case .verified = status.renewalInfo,
|
|
316
|
-
case .verified = status.transaction else { continue }
|
|
317
|
-
if status.state == .subscribed || status.state == .inGracePeriod {
|
|
318
|
-
return true
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
return false
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### Renewal States
|
|
162
|
+
Read status with `Product.SubscriptionInfo.status(for: groupID)`; treat
|
|
163
|
+
`.subscribed` and `.inGracePeriod` as active.
|
|
326
164
|
|
|
327
165
|
| State | Meaning |
|
|
328
166
|
|---|---|
|
|
@@ -332,6 +170,10 @@ func checkSubscriptionActive(groupID: String) async throws -> Bool {
|
|
|
332
170
|
| `.inGracePeriod` | Payment failed but access continues during grace period |
|
|
333
171
|
| `.revoked` | Apple refunded or revoked the subscription |
|
|
334
172
|
|
|
173
|
+
Full status-check helper: references/core-patterns.md#subscription-status-checking.
|
|
174
|
+
For renewal-state access decisions, expiration reasons, grace period, and billing
|
|
175
|
+
retry see [references/storekit-advanced.md](references/storekit-advanced.md).
|
|
176
|
+
|
|
335
177
|
## Restore Purchases
|
|
336
178
|
|
|
337
179
|
StoreKit 2 handles restoration via `Transaction.currentEntitlements`. Add a
|
|
@@ -346,173 +188,46 @@ func restorePurchases() async throws {
|
|
|
346
188
|
|
|
347
189
|
On store views: `.storeButton(.visible, for: .restorePurchases)`
|
|
348
190
|
|
|
349
|
-
## App Transaction
|
|
191
|
+
## App Transaction
|
|
350
192
|
|
|
351
|
-
Verify the legitimacy of the app installation. Use
|
|
352
|
-
or detecting tampered installations (iOS 16+)
|
|
193
|
+
Verify the legitimacy of the app installation with `AppTransaction.shared`. Use
|
|
194
|
+
for business model changes or detecting tampered installations (iOS 16+); read
|
|
195
|
+
`originalAppVersion` / `originalPurchaseDate` on the verified result and restrict
|
|
196
|
+
features on `.unverified`.
|
|
353
197
|
|
|
354
|
-
|
|
355
|
-
func verifyAppPurchase() async {
|
|
356
|
-
do {
|
|
357
|
-
let result = try await AppTransaction.shared
|
|
358
|
-
switch result {
|
|
359
|
-
case .verified(let appTransaction):
|
|
360
|
-
let originalVersion = appTransaction.originalAppVersion
|
|
361
|
-
let purchaseDate = appTransaction.originalPurchaseDate
|
|
362
|
-
// Migration logic for users who paid before subscription model
|
|
363
|
-
case .unverified:
|
|
364
|
-
// Potentially tampered -- restrict features as appropriate
|
|
365
|
-
break
|
|
366
|
-
}
|
|
367
|
-
} catch { /* Could not retrieve app transaction */ }
|
|
368
|
-
}
|
|
369
|
-
```
|
|
198
|
+
Full `verifyAppPurchase()` example: references/core-patterns.md#app-transaction
|
|
370
199
|
|
|
371
200
|
## Purchase Options
|
|
372
201
|
|
|
373
202
|
```swift
|
|
374
|
-
|
|
375
|
-
try await product.purchase(options: [.
|
|
376
|
-
|
|
377
|
-
// Consumable quantity
|
|
378
|
-
try await product.purchase(options: [.quantity(5)])
|
|
379
|
-
|
|
380
|
-
// Simulate Ask to Buy in sandbox
|
|
381
|
-
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])
|
|
203
|
+
try await product.purchase(options: [.appAccountToken(UUID())]) // server reconciliation
|
|
204
|
+
try await product.purchase(options: [.quantity(5)]) // consumable quantity
|
|
205
|
+
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)]) // sandbox
|
|
382
206
|
```
|
|
383
207
|
|
|
384
208
|
## SwiftUI Purchase Callbacks
|
|
385
209
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
.onInAppPurchaseCompletion { product, result in
|
|
391
|
-
if case .success(.success(.verified(let transaction))) = result {
|
|
392
|
-
await deliverContent(for: transaction)
|
|
393
|
-
await transaction.finish()
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
.inAppPurchaseOptions { product in
|
|
397
|
-
[.appAccountToken(userAccountToken)]
|
|
398
|
-
}
|
|
399
|
-
```
|
|
210
|
+
Store views expose `.onInAppPurchaseStart`, `.onInAppPurchaseCompletion`, and
|
|
211
|
+
`.inAppPurchaseOptions`. In the completion handler, verify the transaction,
|
|
212
|
+
deliver content, then `finish()`.
|
|
400
213
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
### 1. Not starting Transaction.updates at app launch
|
|
404
|
-
|
|
405
|
-
```swift
|
|
406
|
-
// WRONG: No listener -- misses renewals, refunds, Ask to Buy approvals
|
|
407
|
-
@main struct MyApp: App {
|
|
408
|
-
var body: some Scene { WindowGroup { ContentView() } }
|
|
409
|
-
}
|
|
410
|
-
// CORRECT: Start listener in App init (see Transaction.updates section above)
|
|
411
|
-
```
|
|
412
|
-
|
|
413
|
-
### 2. Forgetting transaction.finish()
|
|
414
|
-
|
|
415
|
-
```swift
|
|
416
|
-
// WRONG: Never finished -- reappears in unfinished queue forever
|
|
417
|
-
let transaction = try checkVerified(verification)
|
|
418
|
-
unlockFeature(transaction.productID)
|
|
419
|
-
|
|
420
|
-
// CORRECT: Deliver durably, then finish. If delivery fails, do not finish yet.
|
|
421
|
-
let transaction = try checkVerified(verification)
|
|
422
|
-
try await recordDelivery(transaction)
|
|
423
|
-
await transaction.finish()
|
|
424
|
-
```
|
|
425
|
-
|
|
426
|
-
### 3. Ignoring verification result
|
|
427
|
-
|
|
428
|
-
```swift
|
|
429
|
-
// WRONG: Using unverified transaction -- security risk
|
|
430
|
-
let transaction = verification.unsafePayloadValue
|
|
431
|
-
|
|
432
|
-
// CORRECT: Verify before using
|
|
433
|
-
let transaction = try checkVerified(verification)
|
|
434
|
-
```
|
|
435
|
-
|
|
436
|
-
### 4. Using original In-App Purchase APIs in new StoreKit 2 code
|
|
437
|
-
|
|
438
|
-
```swift
|
|
439
|
-
// AVOID: Original In-App Purchase APIs
|
|
440
|
-
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
|
|
441
|
-
SKPaymentQueue.default().add(payment)
|
|
442
|
-
|
|
443
|
-
// PREFERRED: StoreKit 2
|
|
444
|
-
let products = try await Product.products(for: ["com.app.premium"])
|
|
445
|
-
let result = try await product.purchase()
|
|
446
|
-
```
|
|
447
|
-
|
|
448
|
-
### 5. Not checking revocationDate
|
|
449
|
-
|
|
450
|
-
```swift
|
|
451
|
-
// WRONG: Grants access to refunded purchases
|
|
452
|
-
if case .verified(let transaction) = result {
|
|
453
|
-
purchased.insert(transaction.productID)
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
// CORRECT: Skip revoked transactions
|
|
457
|
-
if case .verified(let transaction) = result, transaction.revocationDate == nil {
|
|
458
|
-
purchased.insert(transaction.productID)
|
|
459
|
-
}
|
|
460
|
-
```
|
|
214
|
+
Full callback example: references/core-patterns.md#swiftui-purchase-callbacks
|
|
461
215
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
```swift
|
|
465
|
-
// WRONG: Wrong for other currencies and regions
|
|
466
|
-
Text("Buy Premium for $4.99")
|
|
467
|
-
|
|
468
|
-
// CORRECT: Localized price from Product
|
|
469
|
-
Text("Buy \(product.displayName) for \(product.displayPrice)")
|
|
470
|
-
```
|
|
471
|
-
|
|
472
|
-
### 7. Not handling .pending purchase result
|
|
473
|
-
|
|
474
|
-
```swift
|
|
475
|
-
// WRONG: Silently drops pending Ask to Buy
|
|
476
|
-
default: break
|
|
477
|
-
|
|
478
|
-
// CORRECT: Explain approval is pending; unlock only after Transaction.updates
|
|
479
|
-
case .pending:
|
|
480
|
-
showPendingApprovalMessage()
|
|
481
|
-
```
|
|
482
|
-
|
|
483
|
-
### 8. Checking entitlements only once at launch
|
|
484
|
-
|
|
485
|
-
```swift
|
|
486
|
-
// WRONG: Check once, never update
|
|
487
|
-
func appDidFinish() { Task { await updateEntitlements() } }
|
|
488
|
-
|
|
489
|
-
// CORRECT: Re-check on Transaction.updates AND on foreground return
|
|
490
|
-
// Transaction.updates listener handles mid-session changes.
|
|
491
|
-
// Also use .task { await storeManager.updateEntitlements() } on content views.
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
### 9. Missing restore purchases button
|
|
495
|
-
|
|
496
|
-
```swift
|
|
497
|
-
// WRONG: No restore option -- App Store rejection risk
|
|
498
|
-
SubscriptionStoreView(groupID: "group_id")
|
|
499
|
-
|
|
500
|
-
// CORRECT
|
|
501
|
-
SubscriptionStoreView(groupID: "group_id")
|
|
502
|
-
.storeButton(.visible, for: .restorePurchases)
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### 10. Subscription views without policy links
|
|
216
|
+
## Common Mistakes
|
|
506
217
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
SubscriptionStoreView(groupID: "group_id")
|
|
218
|
+
The ten most common StoreKit 2 defects, each with a WRONG/CORRECT example, are in
|
|
219
|
+
references/core-patterns.md#common-mistakes:
|
|
510
220
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
221
|
+
1. Not starting `Transaction.updates` at app launch
|
|
222
|
+
2. Forgetting `transaction.finish()`
|
|
223
|
+
3. Ignoring the verification result
|
|
224
|
+
4. Using original In-App Purchase APIs in new StoreKit 2 code
|
|
225
|
+
5. Not checking `revocationDate`
|
|
226
|
+
6. Hardcoding prices instead of `product.displayPrice`
|
|
227
|
+
7. Not handling the `.pending` purchase result
|
|
228
|
+
8. Checking entitlements only once at launch
|
|
229
|
+
9. Missing restore purchases button
|
|
230
|
+
10. Subscription views without Terms / Privacy policy links
|
|
516
231
|
|
|
517
232
|
## Review Checklist
|
|
518
233
|
|
|
@@ -536,8 +251,19 @@ SubscriptionStoreView(groupID: "group_id")
|
|
|
536
251
|
|
|
537
252
|
## References
|
|
538
253
|
|
|
539
|
-
- See [references/
|
|
540
|
-
|
|
254
|
+
- See [references/core-patterns.md](references/core-patterns.md) for full core
|
|
255
|
+
code: purchase flow, launch-time transaction listener, entitlement checking,
|
|
256
|
+
store views, subscription status, app transaction, purchase callbacks, and the
|
|
257
|
+
ten annotated common mistakes. Load it when writing or reviewing a core flow.
|
|
258
|
+
- See [references/storekit-advanced.md](references/storekit-advanced.md) for
|
|
259
|
+
subscription control styles, offer management (introductory, promotional,
|
|
260
|
+
win-back, offer codes), server-side validation, StoreKit testing, renewal
|
|
261
|
+
states, grace period/billing retry, refunds, and Family Sharing. Load it for
|
|
262
|
+
offers, testing setup, or advanced subscription handling.
|
|
263
|
+
- See [references/app-review-guidelines.md](references/app-review-guidelines.md)
|
|
264
|
+
for IAP rules (Guideline 3.1.1), subscription display requirements, and
|
|
265
|
+
rejection prevention. Load it before a submission or when diagnosing a payment
|
|
266
|
+
rejection.
|
|
541
267
|
- For submission, privacy, metadata, screenshots, and rejection-risk audits use `app-store-review`.
|
|
542
268
|
- For keyword, screenshot-caption, ranking, and conversion strategy use `app-store-optimization`.
|
|
543
269
|
- Official Apple docs: [Choosing a StoreKit API](https://sosumi.ai/documentation/storekit/choosing-a-storekit-api-for-in-app-purchases), [Transaction.updates](https://sosumi.ai/documentation/storekit/transaction/updates), [Transaction.currentEntitlements](https://sosumi.ai/documentation/storekit/transaction/currententitlements),
|