@mmerterden/multi-agent-pipeline 20.0.0 → 20.2.0
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 +55 -0
- package/README.md +5 -5
- package/README.tr.md +5 -5
- package/SECURITY.md +3 -3
- package/docs/adr/0011-dormant-ci.md +10 -1
- package/docs/architecture.md +2 -2
- package/docs/ecosystem.md +5 -5
- package/docs/facts.json +8 -7
- package/install/_codex-agents.mjs +1 -1
- package/manifest.json +92 -64
- package/package.json +1 -1
- package/pipeline/agents/code-reviewer.md +2 -2
- package/pipeline/agents/dev-critic.md +5 -5
- package/pipeline/agents/security-auditor.md +80 -72
- package/pipeline/commands/figma-to-swiftui.md +1 -1
- package/pipeline/commands/multi-agent/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/channels/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +1 -1
- package/pipeline/commands/multi-agent/help/SKILL.md +2 -0
- package/pipeline/commands/multi-agent/scan/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/security-review/SKILL.md +52 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
- package/pipeline/multi-agent-refs/component-dispatch.md +5 -5
- package/pipeline/multi-agent-refs/cross-cli-contract.md +6 -6
- package/pipeline/multi-agent-refs/features/security-audit.md +55 -0
- package/pipeline/multi-agent-refs/phases/modes.md +1 -1
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +9 -15
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +1 -1
- package/pipeline/multi-agent-refs/threat-model.md +39 -0
- package/pipeline/schemas/agent-state.schema.json +23 -0
- package/pipeline/schemas/phases.json +1 -2
- package/pipeline/schemas/prefs.schema.json +0 -4
- package/pipeline/schemas/reviewer-output.schema.json +99 -2
- package/pipeline/schemas/security-finding.schema.json +144 -0
- package/pipeline/scripts/_stack-routing.mjs +1 -0
- package/pipeline/scripts/gc-abandoned.sh +16 -9
- package/pipeline/scripts/render-work-summary.sh +7 -4
- package/pipeline/skills/.skill-manifest.json +47 -23
- package/pipeline/skills/.skills-index.json +75 -9
- package/pipeline/skills/shared/README.md +13 -7
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +3 -4
- package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +2 -2
- package/pipeline/skills/shared/core/multi-agent-security-review/SKILL.md +29 -0
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
- 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/security-review/SKILL.md +64 -0
- package/pipeline/skills/shared/external/security-review/references/owasp-mobile-top10-2024.md +53 -0
- package/pipeline/skills/shared/external/security-review/references/owasp-web-api-top10-2021.md +56 -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
- package/pipeline/commands/security-review.md +0 -6
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# OWASP Mobile Top 10 2024 - review checklist with CWE mapping
|
|
2
|
+
|
|
3
|
+
The category id (`M1:2024` .. `M10:2024`) goes in a finding's `security.owaspCategory`; the CWE that names the actual weakness goes in `security.cwe`. For the fuller mobile catalog that ties these to MASVS / MASTG and the store-compliance rules, the swift-security compliance mapping is the deeper reference; this is the review checklist.
|
|
4
|
+
|
|
5
|
+
## M1:2024 - Improper Credential Usage
|
|
6
|
+
|
|
7
|
+
Check: hardcoded API keys / passwords / tokens, credentials in source or resource files, credentials in logs, a secret shipped in the binary.
|
|
8
|
+
CWEs: CWE-798 (hardcoded credentials), CWE-259, CWE-522.
|
|
9
|
+
|
|
10
|
+
## M2:2024 - Inadequate Supply Chain Security
|
|
11
|
+
|
|
12
|
+
Check: a dependency at a version with a known advisory, an unpinned SDK, a build step pulling an unverified artifact. This is the `security_dep_inventory` -> CVE path (`A06:2021`'s mobile analog).
|
|
13
|
+
CWEs: CWE-1104, plus the advisory's CWE.
|
|
14
|
+
|
|
15
|
+
## M3:2024 - Insecure Authentication/Authorization
|
|
16
|
+
|
|
17
|
+
Check: auth decided client-side, a token accepted without verification, missing authorization on a sensitive action, biometric gate that only hides UI without protecting data.
|
|
18
|
+
CWEs: CWE-287, CWE-306 (missing authentication), CWE-862, CWE-863.
|
|
19
|
+
|
|
20
|
+
## M4:2024 - Insufficient Input/Output Validation
|
|
21
|
+
|
|
22
|
+
Check: untrusted input into a SQL/content-provider query, a WebView `evaluateJavascript` / `postMessage` handler trusting page content, deep-link parameters used without validation, format-string or path built from input.
|
|
23
|
+
CWEs: CWE-20, CWE-79 (WebView XSS), CWE-89, CWE-22.
|
|
24
|
+
|
|
25
|
+
## M5:2024 - Insecure Communication
|
|
26
|
+
|
|
27
|
+
Check: HTTP instead of HTTPS, disabled ATS / cleartext-traffic permitted, `TrustManager` that accepts all certs, missing certificate pinning on a sensitive endpoint, ignored TLS errors.
|
|
28
|
+
CWEs: CWE-319 (cleartext), CWE-295 (improper certificate validation).
|
|
29
|
+
|
|
30
|
+
## M6:2024 - Inadequate Privacy Controls
|
|
31
|
+
|
|
32
|
+
Check: PII collected without need, location/contacts/identifiers sent off-device without disclosure, tracking before consent, PII in logs or analytics events.
|
|
33
|
+
CWEs: CWE-359, CWE-200, CWE-532.
|
|
34
|
+
|
|
35
|
+
## M7:2024 - Insufficient Binary Protections
|
|
36
|
+
|
|
37
|
+
Check: no tamper/integrity check where the threat model needs one, debug symbols or verbose logging left in a release build, an easily-reversible secret embedded in the binary. Judge against the threat model - most apps do not need anti-reversing, and a `blocking` here needs a named attacker.
|
|
38
|
+
CWEs: CWE-656, CWE-489 (debug code left in).
|
|
39
|
+
|
|
40
|
+
## M8:2024 - Security Misconfiguration
|
|
41
|
+
|
|
42
|
+
Check: an exported Android component with no permission, `android:debuggable=true` or `allowBackup=true` on sensitive data, an overly-broad entitlement, a permissive `network_security_config`, default or weak settings.
|
|
43
|
+
CWEs: CWE-16, CWE-276 (incorrect default permissions), CWE-926 (improper export).
|
|
44
|
+
|
|
45
|
+
## M9:2024 - Insecure Data Storage
|
|
46
|
+
|
|
47
|
+
Check: sensitive data in `UserDefaults` / `SharedPreferences` / plist / plain files instead of the Keychain / Keystore, a database without encryption, a cache holding secrets, pasteboard leakage.
|
|
48
|
+
CWEs: CWE-312 (cleartext storage), CWE-922 (insecure storage of sensitive info).
|
|
49
|
+
|
|
50
|
+
## M10:2024 - Insufficient Cryptography
|
|
51
|
+
|
|
52
|
+
Check: weak or deprecated algorithm (MD5/SHA1 for integrity, DES/ECB), a hardcoded key/IV, a home-rolled cipher, a predictable random source for a security purpose.
|
|
53
|
+
CWEs: CWE-327 (broken/risky algorithm), CWE-326, CWE-330 (insufficient randomness), CWE-338.
|
package/pipeline/skills/shared/external/security-review/references/owasp-web-api-top10-2021.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# OWASP Top 10 2021 (Web / API) - review checklist with CWE mapping
|
|
2
|
+
|
|
3
|
+
The category id (`A01:2021` .. `A10:2021`) goes in a finding's `security.owaspCategory`; the CWE most associated with the specific weakness goes in `security.cwe`. One weakness per finding. The CWEs listed per category are the common ones, not the whole set - pick the one that names the actual defect.
|
|
4
|
+
|
|
5
|
+
## A01:2021 - Broken Access Control
|
|
6
|
+
|
|
7
|
+
Check: missing authorization on a route or action, IDOR (object id from the request trusted without an ownership check), path traversal, forced browsing, CORS misconfiguration allowing credentialed cross-origin reads, privilege escalation through a mass-assignable field.
|
|
8
|
+
CWEs: CWE-284, CWE-285, CWE-639 (IDOR), CWE-862 (missing authorization), CWE-863 (incorrect authorization), CWE-22 (path traversal), CWE-352 (CSRF).
|
|
9
|
+
Evidence to cite: the handler that reads an id from input and queries without a `where owner = current_user` predicate; a route with no auth middleware.
|
|
10
|
+
|
|
11
|
+
## A02:2021 - Cryptographic Failures
|
|
12
|
+
|
|
13
|
+
Check: secrets or PII sent or stored in clear, weak or deprecated algorithms (MD5, SHA1 for passwords, DES, ECB), hardcoded keys, missing TLS, disabled certificate validation, predictable IVs/nonces, passwords hashed without a slow KDF (bcrypt/scrypt/argon2).
|
|
14
|
+
CWEs: CWE-311 (missing encryption), CWE-319 (cleartext transmission), CWE-327 (broken/risky algorithm), CWE-326 (inadequate strength), CWE-798 (hardcoded credentials), CWE-916 (weak password hash).
|
|
15
|
+
|
|
16
|
+
## A03:2021 - Injection
|
|
17
|
+
|
|
18
|
+
Check: SQL/NoSQL/ORM query built by string concatenation of untrusted input, OS command built from input, LDAP/XPath injection, unsanitized input reflected into HTML (XSS), template injection, header injection.
|
|
19
|
+
CWEs: CWE-89 (SQL), CWE-78 (OS command), CWE-79 (XSS), CWE-90 (LDAP), CWE-94 (code injection), CWE-943 (NoSQL/query).
|
|
20
|
+
Evidence: the untrusted source and the sink on the same path, with no parameterization or encoding between.
|
|
21
|
+
|
|
22
|
+
## A04:2021 - Insecure Design
|
|
23
|
+
|
|
24
|
+
Check: a missing control the design needed - no rate limit on a credential endpoint, no anti-automation on a costly action, trust placed in a client-supplied value that decides server behaviour, a workflow that can be replayed.
|
|
25
|
+
CWEs: CWE-73, CWE-183, CWE-209 (info leak by design), CWE-256, CWE-501 (trust boundary violation), CWE-522.
|
|
26
|
+
|
|
27
|
+
## A05:2021 - Security Misconfiguration
|
|
28
|
+
|
|
29
|
+
Check: debug or verbose errors in production, default credentials, an unnecessary feature or port enabled, permissive CORS, missing security headers (CSP, HSTS, X-Content-Type-Options), directory listing, an overly-permissive cloud bucket or IAM policy in config.
|
|
30
|
+
CWEs: CWE-16, CWE-611 (XXE), CWE-732 (incorrect permissions), CWE-1032, CWE-756.
|
|
31
|
+
|
|
32
|
+
## A06:2021 - Vulnerable and Outdated Components
|
|
33
|
+
|
|
34
|
+
Check: a dependency at a version with a known advisory. This is the `security_dep_inventory` -> CVE-lookup path. Carry the `cve` and the advisory's CWE; put the finding on the manifest at line 0.
|
|
35
|
+
CWEs: CWE-1104, plus the advisory's own CWE.
|
|
36
|
+
|
|
37
|
+
## A07:2021 - Identification and Authentication Failures
|
|
38
|
+
|
|
39
|
+
Check: credential stuffing possible (no throttle/lockout), weak password policy, session id in the URL, session not rotated on login, missing or weak MFA, JWT with `alg:none` accepted or signature not verified, long-lived non-revocable tokens.
|
|
40
|
+
CWEs: CWE-287 (improper auth), CWE-297, CWE-384 (session fixation), CWE-521 (weak password), CWE-613 (insufficient expiration), CWE-347 (improper signature verification).
|
|
41
|
+
|
|
42
|
+
## A08:2021 - Software and Data Integrity Failures
|
|
43
|
+
|
|
44
|
+
Check: insecure deserialization of untrusted data, an update or plugin loaded without signature verification, a CI/CD step pulling an unpinned or unverified artifact, client-side data trusted without integrity check.
|
|
45
|
+
CWEs: CWE-502 (deserialization), CWE-345, CWE-494 (download without integrity check), CWE-829.
|
|
46
|
+
|
|
47
|
+
## A09:2021 - Security Logging and Monitoring Failures
|
|
48
|
+
|
|
49
|
+
Check: security-relevant events not logged (auth failures, access-control denials, high-value actions), logs containing secrets or PII, no alerting path. Note it as a `suggestion`/`important` gap, rarely `blocking` on its own.
|
|
50
|
+
CWEs: CWE-778 (insufficient logging), CWE-532 (secrets in logs), CWE-223.
|
|
51
|
+
|
|
52
|
+
## A10:2021 - Server-Side Request Forgery (SSRF)
|
|
53
|
+
|
|
54
|
+
Check: the server fetches a URL built from user input without an allowlist, letting a caller reach internal services, cloud metadata endpoints, or the loopback interface.
|
|
55
|
+
CWEs: CWE-918.
|
|
56
|
+
Evidence: the input-derived URL reaching an HTTP client with no host allowlist or scheme restriction.
|
|
@@ -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),
|