@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.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 +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,804 +1,511 @@
|
|
|
1
|
-
# StoreKit 2 Advanced
|
|
1
|
+
# StoreKit 2 Advanced Patterns
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from the top-level StoreKit skill are in place.
|
|
3
|
+
Reach for this file once purchasing, the launch-time listener and entitlement
|
|
4
|
+
checks from [core-patterns.md](core-patterns.md) are in place. Each heading
|
|
5
|
+
names the release an API needs; the skill as a whole targets iOS 26.
|
|
7
6
|
|
|
8
7
|
## Contents
|
|
9
8
|
|
|
10
|
-
- [
|
|
11
|
-
- [
|
|
9
|
+
- [Paywall Control Styles](#paywall-control-styles)
|
|
10
|
+
- [Where the Controls Sit](#where-the-controls-sit)
|
|
11
|
+
- [Button Labels](#button-labels)
|
|
12
|
+
- [Container Backgrounds](#container-backgrounds)
|
|
13
|
+
- [Store Buttons](#store-buttons)
|
|
14
|
+
- [Policy Destinations](#policy-destinations)
|
|
15
|
+
- [An Icon per Option](#an-icon-per-option)
|
|
16
|
+
- [Sign-In Action](#sign-in-action)
|
|
17
|
+
- [Grouping Options (iOS 18+)](#grouping-options-ios-18)
|
|
12
18
|
- [Introductory Offers](#introductory-offers)
|
|
13
19
|
- [Promotional Offers](#promotional-offers)
|
|
14
|
-
- [Win-Back Offers](#win-back-offers)
|
|
20
|
+
- [Win-Back Offers (iOS 18+)](#win-back-offers-ios-18)
|
|
15
21
|
- [Offer Codes](#offer-codes)
|
|
22
|
+
- [Which Offer a Transaction Used](#which-offer-a-transaction-used)
|
|
16
23
|
- [Server-Side Validation](#server-side-validation)
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
24
|
+
- [Testing in Xcode](#testing-in-xcode)
|
|
25
|
+
- [Renewal States and What to Grant](#renewal-states-and-what-to-grant)
|
|
19
26
|
- [Grace Period and Billing Retry](#grace-period-and-billing-retry)
|
|
20
|
-
- [
|
|
27
|
+
- [Refunds](#refunds)
|
|
21
28
|
- [Family Sharing](#family-sharing)
|
|
22
|
-
- [Ask to Buy
|
|
23
|
-
- [
|
|
24
|
-
- [Subscription Status
|
|
25
|
-
- [Product
|
|
26
|
-
- [Price
|
|
29
|
+
- [Ask to Buy](#ask-to-buy)
|
|
30
|
+
- [Task Modifiers](#task-modifiers)
|
|
31
|
+
- [Live Subscription Status](#live-subscription-status)
|
|
32
|
+
- [Promoted Purchases on the Product Page](#promoted-purchases-on-the-product-page)
|
|
33
|
+
- [Price Increases](#price-increases)
|
|
27
34
|
- [Unfinished Transactions](#unfinished-transactions)
|
|
28
|
-
- [
|
|
35
|
+
- [Advanced Mistakes](#advanced-mistakes)
|
|
29
36
|
|
|
30
|
-
##
|
|
37
|
+
## Paywall Control Styles
|
|
31
38
|
|
|
32
|
-
|
|
33
|
-
`SubscriptionStoreView`.
|
|
39
|
+
`.subscriptionStoreControlStyle(_:)` chooses how plans are presented.
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// Full-page swipeable picker (one option per page)
|
|
46
|
-
.subscriptionStoreControlStyle(.pagedPicker)
|
|
47
|
-
|
|
48
|
-
// Paged picker with prominent selected option
|
|
49
|
-
.subscriptionStoreControlStyle(.pagedProminentPicker)
|
|
50
|
-
|
|
51
|
-
// Minimal inline picker for tight layouts
|
|
52
|
-
.subscriptionStoreControlStyle(.compactPicker)
|
|
53
|
-
|
|
54
|
-
// System decides based on context
|
|
55
|
-
.subscriptionStoreControlStyle(.automatic)
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Control Placement
|
|
59
|
-
|
|
60
|
-
Specify where controls appear within the view. Not every placement is supported
|
|
61
|
-
by every control style, so let unsupported combinations fall back to the system
|
|
62
|
-
default instead of assuming exact placement on every platform.
|
|
63
|
-
|
|
64
|
-
```swift
|
|
65
|
-
.subscriptionStoreControlStyle(.picker, placement: .bottom)
|
|
66
|
-
// Placement options: .bottom, .leading, .trailing, .scrollView,
|
|
67
|
-
// .bottomBar, .buttonsInBottomBar
|
|
68
|
-
```
|
|
41
|
+
| Style | Look | Since |
|
|
42
|
+
|-------|------|-------|
|
|
43
|
+
| `.automatic` | the system decides for the context | iOS 17 |
|
|
44
|
+
| `.buttons` | one button per plan | iOS 17 |
|
|
45
|
+
| `.picker` | an inline picker | iOS 17 |
|
|
46
|
+
| `.prominentPicker` | a picker that emphasises the selected plan | iOS 17 |
|
|
47
|
+
| `.pagedPicker` | one plan per swipeable page | iOS 18 |
|
|
48
|
+
| `.pagedProminentPicker` | paged, with the selection emphasised | iOS 18 |
|
|
49
|
+
| `.compactPicker` | a minimal picker for tight layouts | iOS 18 |
|
|
69
50
|
|
|
70
|
-
|
|
51
|
+
## Where the Controls Sit
|
|
71
52
|
|
|
72
|
-
|
|
53
|
+
`.subscriptionStoreControlStyle(_:placement:)` (iOS 18) adds a placement. The
|
|
54
|
+
placement type belongs to the style, so the valid values differ:
|
|
73
55
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.subscriptionStoreButtonLabel(.singleLine) // Condensed single line
|
|
80
|
-
.subscriptionStoreButtonLabel(.automatic) // System default
|
|
81
|
-
```
|
|
56
|
+
| Style | Placements on iOS |
|
|
57
|
+
|-------|-------------------|
|
|
58
|
+
| `.picker`, `.prominentPicker` | `.automatic`, `.scrollView`, `.buttonsInBottomBar` |
|
|
59
|
+
| `.pagedPicker`, `.pagedProminentPicker`, `.compactPicker` | `.automatic`, `.scrollView`, `.bottomBar`, `.buttonsInBottomBar` |
|
|
60
|
+
| `.automatic` | `.automatic`, `.scrollView`, `.bottomBar`, `.buttonsInBottomBar` |
|
|
82
61
|
|
|
83
|
-
|
|
62
|
+
`.leading`, `.trailing` and `.bottom` exist only on tvOS, so
|
|
63
|
+
`.subscriptionStoreControlStyle(.picker, placement: .bottom)` does not compile
|
|
64
|
+
for iOS. Even a valid placement is a request: the system can fall back to its
|
|
65
|
+
default on some platforms, so do not promise an exact position.
|
|
84
66
|
|
|
85
67
|
```swift
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Image("premium-header").resizable().scaledToFill()
|
|
90
|
-
}
|
|
68
|
+
@available(iOS 18, *)
|
|
69
|
+
struct PagedPlans: View {
|
|
70
|
+
let group: String
|
|
91
71
|
|
|
92
|
-
|
|
93
|
-
|
|
72
|
+
var body: some View {
|
|
73
|
+
SubscriptionStoreView(groupID: group)
|
|
74
|
+
.subscriptionStoreControlStyle(.pagedPicker, placement: .bottomBar)
|
|
75
|
+
}
|
|
94
76
|
}
|
|
95
77
|
```
|
|
96
78
|
|
|
97
|
-
|
|
79
|
+
## Button Labels
|
|
98
80
|
|
|
99
|
-
|
|
100
|
-
.storeButton(.visible, for: .restorePurchases)
|
|
101
|
-
.storeButton(.visible, for: .redeemCode)
|
|
102
|
-
.storeButton(.visible, for: .cancellation)
|
|
103
|
-
.storeButton(.visible, for: .policies)
|
|
104
|
-
.storeButton(.hidden, for: .signIn)
|
|
105
|
-
```
|
|
81
|
+
`.subscriptionStoreButtonLabel(_:)` (iOS 17):
|
|
106
82
|
|
|
107
|
-
|
|
83
|
+
| Label | Shows |
|
|
84
|
+
|-------|-------|
|
|
85
|
+
| `.multiline` | name, price and period |
|
|
86
|
+
| `.price` | only the price |
|
|
87
|
+
| `.displayName` | only the plan name |
|
|
88
|
+
| `.action` | a verb such as "Subscribe" |
|
|
89
|
+
| `.singleLine` | everything squeezed onto one line |
|
|
90
|
+
| `.automatic` | the system default |
|
|
108
91
|
|
|
109
|
-
|
|
110
|
-
// URL-based
|
|
111
|
-
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
|
|
112
|
-
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
|
|
113
|
-
|
|
114
|
-
// Custom view
|
|
115
|
-
.subscriptionStorePolicyDestination(for: .termsOfService) {
|
|
116
|
-
TermsOfServiceView()
|
|
117
|
-
}
|
|
92
|
+
## Container Backgrounds
|
|
118
93
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
94
|
+
| Call | Paints |
|
|
95
|
+
|------|--------|
|
|
96
|
+
| `.containerBackground(.indigo.gradient, for: .subscriptionStore)` | the whole paywall, from a style |
|
|
97
|
+
| `.containerBackground(for: .subscriptionStoreHeader) { Image("hero").resizable() }` | only the header area |
|
|
98
|
+
| `.containerBackground(for: .subscriptionStoreFullHeight) { LinearGradient(...) }` | full height, behind everything |
|
|
124
99
|
|
|
125
|
-
|
|
126
|
-
.subscriptionStoreControlIcon { product, subscriptionInfo in
|
|
127
|
-
if subscriptionInfo.subscriptionPeriod.unit == .year {
|
|
128
|
-
Image(systemName: "star.fill")
|
|
129
|
-
} else {
|
|
130
|
-
Image(systemName: "star")
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
```
|
|
100
|
+
## Store Buttons
|
|
134
101
|
|
|
135
|
-
|
|
102
|
+
`.storeButton(_:for:)` shows or hides system buttons. Kinds:
|
|
103
|
+
`.restorePurchases`, `.redeemCode`, `.cancellation`, `.policies`, `.signIn`.
|
|
104
|
+
Visibility is `.visible` or `.hidden`.
|
|
136
105
|
|
|
137
106
|
```swift
|
|
138
|
-
|
|
139
|
-
.
|
|
140
|
-
|
|
141
|
-
}
|
|
107
|
+
SubscriptionStoreView(groupID: group)
|
|
108
|
+
.storeButton(.visible, for: .restorePurchases, .redeemCode)
|
|
109
|
+
.storeButton(.hidden, for: .signIn)
|
|
142
110
|
```
|
|
143
111
|
|
|
144
|
-
##
|
|
112
|
+
## Policy Destinations
|
|
145
113
|
|
|
146
|
-
|
|
147
|
-
|
|
114
|
+
- By URL: `.subscriptionStorePolicyDestination(url:for:)` with
|
|
115
|
+
`.termsOfService` or `.privacyPolicy`.
|
|
116
|
+
- By view: `.subscriptionStorePolicyDestination(for: .termsOfService) { TermsView() }`.
|
|
117
|
+
- Link colour: `.subscriptionStorePolicyForegroundStyle(.secondary)`.
|
|
148
118
|
|
|
149
|
-
|
|
119
|
+
## An Icon per Option
|
|
150
120
|
|
|
151
121
|
```swift
|
|
152
|
-
SubscriptionStoreView(groupID:
|
|
153
|
-
|
|
154
|
-
|
|
122
|
+
SubscriptionStoreView(groupID: group)
|
|
123
|
+
.subscriptionStoreControlIcon { _, info in
|
|
124
|
+
Image(systemName: info.subscriptionPeriod.unit == .year ? "star.circle.fill" : "calendar")
|
|
155
125
|
}
|
|
156
|
-
SubscriptionOptionGroup("Annual Plans") { product in
|
|
157
|
-
product.subscription?.subscriptionPeriod.unit == .year
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
126
|
```
|
|
161
127
|
|
|
162
|
-
|
|
128
|
+
## Sign-In Action
|
|
163
129
|
|
|
164
|
-
|
|
165
|
-
SubscriptionStoreView(groupID: "premium_group") {
|
|
166
|
-
SubscriptionPeriodGroupSet()
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Sections with Headers
|
|
130
|
+
Show the system sign-in button and decide what it does:
|
|
171
131
|
|
|
172
132
|
```swift
|
|
173
|
-
SubscriptionStoreView(groupID:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
Text("Basic features included")
|
|
178
|
-
}
|
|
179
|
-
SubscriptionOptionSection("Pro", isIncluded: { product in
|
|
180
|
-
product.subscription?.groupLevel == 2
|
|
181
|
-
}) {
|
|
182
|
-
Text("All features included")
|
|
183
|
-
}
|
|
184
|
-
}
|
|
133
|
+
SubscriptionStoreView(groupID: group)
|
|
134
|
+
.storeButton(.visible, for: .signIn)
|
|
135
|
+
.subscriptionStoreSignInAction { showLogin = true }
|
|
136
|
+
.sheet(isPresented: $showLogin) { LoginSheet() }
|
|
185
137
|
```
|
|
186
138
|
|
|
187
|
-
|
|
139
|
+
## Grouping Options (iOS 18+)
|
|
188
140
|
|
|
189
|
-
|
|
141
|
+
The hierarchy helpers arrange plans inside `SubscriptionStoreView`; the
|
|
142
|
+
content-builder initializer that takes them is iOS 18.
|
|
190
143
|
|
|
191
144
|
```swift
|
|
192
|
-
|
|
193
|
-
|
|
145
|
+
@available(iOS 18, *)
|
|
146
|
+
struct TieredPlans: View {
|
|
147
|
+
let group: String
|
|
194
148
|
|
|
195
|
-
|
|
196
|
-
SubscriptionStoreView(groupID:
|
|
149
|
+
var body: some View {
|
|
150
|
+
SubscriptionStoreView(groupID: group) {
|
|
151
|
+
SubscriptionOptionGroup("Monthly") { $0.subscription?.subscriptionPeriod.unit == .month }
|
|
152
|
+
SubscriptionOptionGroup("Yearly") { $0.subscription?.subscriptionPeriod.unit == .year }
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
197
156
|
```
|
|
198
157
|
|
|
199
|
-
|
|
158
|
+
- `SubscriptionOptionGroup(_:isIncluded:)` groups plans by a predicate.
|
|
159
|
+
- `SubscriptionPeriodGroupSet()` groups by billing period for you.
|
|
160
|
+
- `SubscriptionOptionSection(_:isIncluded:)` adds a section with a header;
|
|
161
|
+
the predicate can look at `product.subscription?.groupLevel` to split tiers.
|
|
162
|
+
|
|
163
|
+
`SubscriptionStoreView(groupID:visibleRelationships:)` is iOS 17: `.all`
|
|
164
|
+
lists every plan, `.upgrade` only plans above the current one.
|
|
200
165
|
|
|
201
166
|
`SubscriptionStoreButton`, `SubscriptionStorePicker`,
|
|
202
|
-
`SubscriptionOptionGroup
|
|
203
|
-
|
|
204
|
-
them as
|
|
167
|
+
`SubscriptionOptionGroup` and their relatives are building blocks for a custom
|
|
168
|
+
`SubscriptionStoreControlStyle` and belong inside its
|
|
169
|
+
`makeBody(configuration:)`. Placing them as free-standing content outside a
|
|
170
|
+
control style is not supported.
|
|
205
171
|
|
|
206
172
|
## Introductory Offers
|
|
207
173
|
|
|
208
|
-
### Checking Eligibility
|
|
209
|
-
|
|
210
174
|
```swift
|
|
211
|
-
|
|
212
|
-
let
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
for: groupID
|
|
217
|
-
)
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Accessing Offer Details
|
|
221
|
-
|
|
222
|
-
```swift
|
|
223
|
-
if let introOffer = product.subscription?.introductoryOffer {
|
|
224
|
-
let price = introOffer.displayPrice // Localized price
|
|
225
|
-
let period = introOffer.period // SubscriptionPeriod
|
|
226
|
-
let count = introOffer.periodCount // Number of periods
|
|
227
|
-
let mode = introOffer.paymentMode // .freeTrial, .payAsYouGo, .payUpFront
|
|
228
|
-
|
|
229
|
-
switch introOffer.paymentMode {
|
|
175
|
+
func trialLine(for plan: Product, group: String) async -> String? {
|
|
176
|
+
guard let intro = plan.subscription?.introductoryOffer,
|
|
177
|
+
await Product.SubscriptionInfo.isEligibleForIntroOffer(for: group) else { return nil }
|
|
178
|
+
let length = "\(intro.periodCount) x \(intro.period.value) \(intro.period.unit)"
|
|
179
|
+
switch intro.paymentMode {
|
|
230
180
|
case .freeTrial:
|
|
231
|
-
|
|
181
|
+
return "Free for \(length), then \(plan.displayPrice)"
|
|
232
182
|
case .payAsYouGo:
|
|
233
|
-
|
|
183
|
+
return "\(intro.displayPrice) per period for \(length)"
|
|
234
184
|
case .payUpFront:
|
|
235
|
-
|
|
185
|
+
return "\(intro.displayPrice) up front for \(length)"
|
|
236
186
|
default:
|
|
237
|
-
|
|
187
|
+
return nil
|
|
238
188
|
}
|
|
239
189
|
}
|
|
240
190
|
```
|
|
241
191
|
|
|
242
|
-
|
|
192
|
+
- Eligibility per product: `product.subscription?.isEligibleForIntroOffer`,
|
|
193
|
+
treat `nil` as `false`. Per group:
|
|
194
|
+
`await Product.SubscriptionInfo.isEligibleForIntroOffer(for: groupID)`.
|
|
195
|
+
- `introductoryOffer` gives `displayPrice` (localized), `period`
|
|
196
|
+
(`Product.SubscriptionPeriod`), `periodCount` and `paymentMode`.
|
|
197
|
+
- An eligible user gets the intro offer automatically; no purchase option is
|
|
198
|
+
involved.
|
|
243
199
|
|
|
244
200
|
## Promotional Offers
|
|
245
201
|
|
|
246
|
-
Promotional offers
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
```swift
|
|
251
|
-
let promoOffers = product.subscription?.promotionalOffers ?? []
|
|
252
|
-
for offer in promoOffers {
|
|
253
|
-
print("Offer: \(offer.id ?? "nil"), price: \(offer.displayPrice)")
|
|
254
|
-
}
|
|
255
|
-
```
|
|
202
|
+
Promotional offers need a signature produced by your server. List them from
|
|
203
|
+
`product.subscription?.promotionalOffers` (empty when `nil`); each has an
|
|
204
|
+
optional `id` and a `displayPrice`.
|
|
256
205
|
|
|
257
|
-
|
|
206
|
+
On iOS 26 the server signs a compact JWS and the app passes it with
|
|
207
|
+
`Product.PurchaseOption.promotionalOffer(_:compactJWS:)`, which returns an
|
|
208
|
+
array of options and is back-deployed:
|
|
258
209
|
|
|
259
210
|
```swift
|
|
260
|
-
|
|
261
|
-
let signature = Product.SubscriptionOffer.Signature(
|
|
262
|
-
keyID: serverKeyID,
|
|
263
|
-
nonce: serverNonce,
|
|
264
|
-
timestamp: serverTimestamp,
|
|
265
|
-
signature: serverSignatureData
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
// 2. Purchase with the offer
|
|
269
|
-
guard let offerID = offer.id else { throw StoreError.invalidOffer }
|
|
270
|
-
let result = try await product.purchase(options: [
|
|
271
|
-
.promotionalOffer(offerID: offerID, signature: signature)
|
|
272
|
-
])
|
|
273
|
-
```
|
|
211
|
+
enum OfferError: Error { case missingID }
|
|
274
212
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
```swift
|
|
281
|
-
.subscriptionPromotionalOffer { product, subscription in
|
|
282
|
-
subscription.promotionalOffers.first
|
|
283
|
-
} compactJWS: { product, subscription, offer in
|
|
284
|
-
guard let offerID = offer.id else { throw StoreError.invalidOffer }
|
|
285
|
-
return try await offerSigner.compactJWS(
|
|
286
|
-
productID: product.id,
|
|
287
|
-
offerID: offerID
|
|
288
|
-
)
|
|
289
|
-
}
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Preferred Offer Selection
|
|
293
|
-
|
|
294
|
-
Let the system choose the best offer for each user:
|
|
295
|
-
|
|
296
|
-
```swift
|
|
297
|
-
.preferredSubscriptionOffer { product, subscription, eligibleOffers in
|
|
298
|
-
// Return the best offer, or nil for no offer
|
|
299
|
-
return eligibleOffers.first
|
|
213
|
+
func redeem(_ offer: Product.SubscriptionOffer, on plan: Product, signer: OfferSigner) async throws -> Product.PurchaseResult {
|
|
214
|
+
guard let offerID = offer.id else { throw OfferError.missingID }
|
|
215
|
+
let jws = try await signer.compactJWS(product: plan.id, offer: offerID)
|
|
216
|
+
let options = Set(Product.PurchaseOption.promotionalOffer(offerID, compactJWS: jws))
|
|
217
|
+
return try await plan.purchase(options: options)
|
|
300
218
|
}
|
|
301
219
|
```
|
|
302
220
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
```swift
|
|
310
|
-
let winBackOffers = product.subscription?.winBackOffers ?? []
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
`winBackOffers` is the raw offer list for the product. Filter it through
|
|
314
|
-
`renewalInfo.eligibleWinBackOfferIDs` before showing or applying offers.
|
|
315
|
-
|
|
316
|
-
### Checking Eligibility via Renewal Info
|
|
317
|
-
|
|
318
|
-
```swift
|
|
319
|
-
let statuses = try await Product.SubscriptionInfo.status(for: groupID)
|
|
320
|
-
let eligibleOfferIDs = statuses.flatMap { status -> [String] in
|
|
321
|
-
guard case .verified(let renewalInfo) = status.renewalInfo else { return [] }
|
|
322
|
-
return renewalInfo.eligibleWinBackOfferIDs
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
let offersByID = Dictionary(uniqueKeysWithValues: winBackOffers.compactMap { offer in
|
|
326
|
-
offer.id.map { ($0, offer) }
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
let eligibleWinBackOffers = eligibleOfferIDs.compactMap { offersByID[$0] }
|
|
330
|
-
for offer in eligibleWinBackOffers {
|
|
331
|
-
// Display or apply only eligible offers.
|
|
332
|
-
}
|
|
333
|
-
```
|
|
221
|
+
The older forms, `.promotionalOffer(offerID:signature:)` with a
|
|
222
|
+
`Product.SubscriptionOffer.Signature(keyID:nonce:timestamp:signature:)`
|
|
223
|
+
(iOS 17.4) and `.promotionalOffer(offerID:keyID:nonce:signature:timestamp:)`
|
|
224
|
+
(iOS 15), are deprecated in iOS 26. Keep them only behind an availability
|
|
225
|
+
check for older targets.
|
|
334
226
|
|
|
335
|
-
|
|
227
|
+
Store views on iOS 26 take the offer and the signature as closures:
|
|
336
228
|
|
|
337
229
|
```swift
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
## Offer Codes
|
|
230
|
+
@available(iOS 26, *)
|
|
231
|
+
struct WinTheUserBack: View {
|
|
232
|
+
let group: String
|
|
233
|
+
let signer: OfferSigner
|
|
344
234
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
Button("Redeem Code") { showRedeemSheet = true }
|
|
352
|
-
.offerCodeRedemption(isPresented: $showRedeemSheet) { result in
|
|
353
|
-
switch result {
|
|
354
|
-
case .success:
|
|
355
|
-
await storeManager.updateEntitlements()
|
|
356
|
-
case .failure(let error):
|
|
357
|
-
print("Redemption failed: \(error)")
|
|
235
|
+
var body: some View {
|
|
236
|
+
SubscriptionStoreView(groupID: group)
|
|
237
|
+
.subscriptionPromotionalOffer { _, subscription in
|
|
238
|
+
subscription.promotionalOffers.first
|
|
239
|
+
} compactJWS: { product, _, offer in
|
|
240
|
+
try await signer.compactJWS(product: product.id, offer: offer.id ?? "")
|
|
358
241
|
}
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
### Show Redeem Button on Subscription Store
|
|
364
|
-
|
|
365
|
-
```swift
|
|
366
|
-
.storeButton(.visible, for: .redeemCode)
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
### Testing Offer Codes
|
|
370
|
-
|
|
371
|
-
Test offer-code redemption in StoreKit configuration files and sandbox. In
|
|
372
|
-
StoreKit testing, use the configured offer-code reference name:
|
|
373
|
-
|
|
374
|
-
```swift
|
|
375
|
-
try await product.purchase(options: [.codeOffer(referenceName: "SUMMER2024")])
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
Also test the user-facing redemption path with `.storeButton(.visible, for:
|
|
379
|
-
.redeemCode)` or `.offerCodeRedemption(isPresented:)`, then verify the resulting
|
|
380
|
-
transaction uses `transaction.offer?.type == .code` and the expected
|
|
381
|
-
`transaction.offer?.id`.
|
|
382
|
-
|
|
383
|
-
### Verifying Applied Offers in Transactions
|
|
384
|
-
|
|
385
|
-
```swift
|
|
386
|
-
if let offer = transaction.offer {
|
|
387
|
-
switch offer.type {
|
|
388
|
-
case .introductory: break // Introductory offer applied
|
|
389
|
-
case .promotional: break // Promotional offer applied
|
|
390
|
-
case .code: break // Offer code redeemed
|
|
391
|
-
case .winBack: break // Win-back offer applied
|
|
392
|
-
default: break
|
|
393
242
|
}
|
|
394
|
-
|
|
395
|
-
let offerID = offer.id
|
|
396
243
|
}
|
|
397
244
|
```
|
|
398
245
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
### Sending JWS to Server
|
|
402
|
-
|
|
403
|
-
StoreKit 2 transactions are JWS (JSON Web Signature) tokens. Send the raw
|
|
404
|
-
JWS string to your server for validation.
|
|
405
|
-
|
|
406
|
-
```swift
|
|
407
|
-
case .success(let verification):
|
|
408
|
-
let transaction = try checkVerified(verification)
|
|
409
|
-
|
|
410
|
-
// Send to server for validation
|
|
411
|
-
let jwsString = verification.jwsRepresentation
|
|
412
|
-
try await sendToServer(jws: jwsString, productID: transaction.productID)
|
|
413
|
-
|
|
414
|
-
await transaction.finish()
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
### Server-Side Verification
|
|
418
|
-
|
|
419
|
-
On your server, use Apple's App Store Server Library:
|
|
420
|
-
- `verifyAndDecodeTransaction(signedTransaction:)` to validate and decode
|
|
421
|
-
- `verifyAndDecodeRenewalInfo(signedRenewalInfo:)` for subscription renewal info
|
|
422
|
-
- The JWS format matches `JWSTransaction` from the App Store Server API and
|
|
423
|
-
App Store Server Notifications V2
|
|
424
|
-
|
|
425
|
-
### Device Verification
|
|
426
|
-
|
|
427
|
-
Bind transactions to specific devices to prevent replay attacks:
|
|
428
|
-
|
|
429
|
-
```swift
|
|
430
|
-
let deviceVerification = transaction.deviceVerification
|
|
431
|
-
let nonce = transaction.deviceVerificationNonce
|
|
432
|
-
// Send both to server for additional validation
|
|
433
|
-
```
|
|
434
|
-
|
|
435
|
-
## StoreKit Testing in Xcode
|
|
436
|
-
|
|
437
|
-
### StoreKit Configuration Files
|
|
438
|
-
|
|
439
|
-
1. Create a StoreKit Configuration file in Xcode: File > New > File >
|
|
440
|
-
StoreKit Configuration File
|
|
441
|
-
2. Add products matching your App Store Connect configuration
|
|
442
|
-
3. Set the configuration in the scheme: Edit Scheme > Run > Options >
|
|
443
|
-
StoreKit Configuration
|
|
444
|
-
|
|
445
|
-
### Configuration File Contents
|
|
446
|
-
|
|
447
|
-
Define products with:
|
|
448
|
-
- Product ID, reference name, product type
|
|
449
|
-
- Price and locale
|
|
450
|
-
- Subscription group and level (for subscriptions)
|
|
451
|
-
- Introductory and promotional offers
|
|
452
|
-
- Family Sharing settings
|
|
453
|
-
|
|
454
|
-
### Testing Features
|
|
455
|
-
|
|
456
|
-
```swift
|
|
457
|
-
// Simulate Ask to Buy
|
|
458
|
-
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])
|
|
459
|
-
|
|
460
|
-
// Test-only: set purchase date and renewal behavior
|
|
461
|
-
try await product.purchase(options: [
|
|
462
|
-
.purchaseDate(Date(), renewalBehavior: .default)
|
|
463
|
-
])
|
|
464
|
-
|
|
465
|
-
// Test-only: apply offer code by reference name
|
|
466
|
-
try await product.purchase(options: [.codeOffer(referenceName: "SUMMER2024")])
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### StoreKit Testing Capabilities
|
|
470
|
-
|
|
471
|
-
- Simulate failed transactions, interrupted purchases, refunds
|
|
472
|
-
- Test Ask to Buy pending approvals for promotional, win-back, and offer-code purchases
|
|
473
|
-
- Speed up subscription renewals (renewals happen in minutes, not months)
|
|
474
|
-
- Test grace period and billing retry states
|
|
475
|
-
- Clear purchase history between test runs
|
|
476
|
-
- Test offer redemption flows
|
|
477
|
-
- Simulate subscription expiration and cancellation
|
|
246
|
+
That replaces `.subscriptionPromotionalOffer(offer:signature:)`, deprecated in
|
|
247
|
+
iOS 26.
|
|
478
248
|
|
|
479
|
-
|
|
249
|
+
`.preferredSubscriptionOffer { product, subscription, eligibleOffers in ... }`
|
|
250
|
+
(iOS 18) picks which eligible offer a store view presents to this user;
|
|
251
|
+
return `nil` for none.
|
|
480
252
|
|
|
481
|
-
|
|
482
|
-
- View all test transactions
|
|
483
|
-
- Delete transactions to reset state
|
|
484
|
-
- Request refunds
|
|
485
|
-
- Expire subscriptions
|
|
486
|
-
- Approve or decline Ask to Buy requests
|
|
253
|
+
## Win-Back Offers (iOS 18+)
|
|
487
254
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
255
|
+
Win-back offers target people who used to subscribe and cancelled.
|
|
256
|
+
`product.subscription?.winBackOffers` (empty when `nil`) is the raw list for
|
|
257
|
+
the product, not for this user: filter it through the verified
|
|
258
|
+
`renewalInfo.eligibleWinBackOfferIDs` before showing or applying anything.
|
|
491
259
|
|
|
492
260
|
```swift
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
showPaymentUpdatePrompt()
|
|
503
|
-
```
|
|
504
|
-
|
|
505
|
-
### Degraded States (consider limited or no access)
|
|
506
|
-
|
|
507
|
-
```swift
|
|
508
|
-
case .inBillingRetryPeriod:
|
|
509
|
-
// Payment failed, Apple is retrying -- access decision is yours
|
|
510
|
-
// Apple recommends granting limited access to encourage payment update
|
|
511
|
-
grantLimitedAccess()
|
|
512
|
-
showPaymentFailedBanner()
|
|
513
|
-
|
|
514
|
-
case .expired:
|
|
515
|
-
// Subscription ended -- check expirationReason
|
|
516
|
-
revokeAccess()
|
|
517
|
-
if let reason = renewalInfo.expirationReason {
|
|
518
|
-
switch reason {
|
|
519
|
-
case .autoRenewDisabled: showResubscribeOffer()
|
|
520
|
-
case .billingError: showUpdatePaymentMethod()
|
|
521
|
-
case .didNotConsentToPriceIncrease: showPriceInfo()
|
|
522
|
-
case .productUnavailable: break
|
|
523
|
-
default: break
|
|
261
|
+
@available(iOS 18, *)
|
|
262
|
+
func eligibleWinBacks(for plan: Product, group: String) async throws -> [Product.SubscriptionOffer] {
|
|
263
|
+
let all = plan.subscription?.winBackOffers ?? []
|
|
264
|
+
let byID = Dictionary(all.compactMap { offer in offer.id.map { ($0, offer) } },
|
|
265
|
+
uniquingKeysWith: { first, _ in first })
|
|
266
|
+
var allowedIDs: [String] = []
|
|
267
|
+
for status in try await Product.SubscriptionInfo.status(for: group) {
|
|
268
|
+
if case .verified(let renewal) = status.renewalInfo {
|
|
269
|
+
allowedIDs.append(contentsOf: renewal.eligibleWinBackOfferIDs)
|
|
524
270
|
}
|
|
525
271
|
}
|
|
526
|
-
|
|
527
|
-
case .revoked:
|
|
528
|
-
// Apple refunded -- must revoke access
|
|
529
|
-
revokeAccess()
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
### Expiration Reasons
|
|
533
|
-
|
|
534
|
-
| Reason | Meaning |
|
|
535
|
-
|---|---|
|
|
536
|
-
| `.autoRenewDisabled` | User voluntarily cancelled |
|
|
537
|
-
| `.billingError` | Payment method failed |
|
|
538
|
-
| `.didNotConsentToPriceIncrease` | User did not agree to price increase |
|
|
539
|
-
| `.productUnavailable` | Product no longer available |
|
|
540
|
-
| `.unknown` | Unspecified reason |
|
|
541
|
-
|
|
542
|
-
## Grace Period and Billing Retry
|
|
543
|
-
|
|
544
|
-
### Grace Period
|
|
545
|
-
|
|
546
|
-
When enabled in App Store Connect, subscribers retain access for a short
|
|
547
|
-
period after a billing failure. Check `status.state == .inGracePeriod` and
|
|
548
|
-
the `gracePeriodExpirationDate` on renewal info.
|
|
549
|
-
|
|
550
|
-
```swift
|
|
551
|
-
if status.state == .inGracePeriod,
|
|
552
|
-
case .verified(let renewalInfo) = status.renewalInfo {
|
|
553
|
-
let expirationDate = renewalInfo.gracePeriodExpirationDate
|
|
554
|
-
// Grant access but prompt to update payment method
|
|
272
|
+
return allowedIDs.compactMap { byID[$0] }
|
|
555
273
|
}
|
|
556
274
|
```
|
|
557
275
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
Apple automatically retries failed payments. During billing retry, the
|
|
561
|
-
subscription state is `.inBillingRetryPeriod`. Check `renewalInfo.isInBillingRetry`.
|
|
562
|
-
|
|
563
|
-
## Refund Handling
|
|
564
|
-
|
|
565
|
-
### Initiating Refund Requests
|
|
566
|
-
|
|
567
|
-
```swift
|
|
568
|
-
// From a transaction instance
|
|
569
|
-
let refundStatus = try await transaction.beginRefundRequest(in: windowScene)
|
|
276
|
+
The result keeps the order of `eligibleWinBackOfferIDs`. Buy with
|
|
277
|
+
`plan.purchase(options: [.winBackOffer(offer)])`.
|
|
570
278
|
|
|
571
|
-
|
|
572
|
-
let refundStatus = try await Transaction.beginRefundRequest(
|
|
573
|
-
for: transactionID, in: windowScene
|
|
574
|
-
)
|
|
575
|
-
```
|
|
576
|
-
|
|
577
|
-
### SwiftUI Refund Sheet
|
|
279
|
+
## Offer Codes
|
|
578
280
|
|
|
579
281
|
```swift
|
|
580
|
-
|
|
282
|
+
struct RedeemRow: View {
|
|
283
|
+
@State private var showRedeem = false
|
|
581
284
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
285
|
+
var body: some View {
|
|
286
|
+
Button("Redeem code") { showRedeem = true }
|
|
287
|
+
.offerCodeRedemption(isPresented: $showRedeem) { result in
|
|
288
|
+
switch result {
|
|
289
|
+
case .success:
|
|
290
|
+
Task { await EntitlementStore.shared.refresh() }
|
|
291
|
+
case .failure:
|
|
292
|
+
showRedeem = false
|
|
293
|
+
}
|
|
294
|
+
}
|
|
585
295
|
}
|
|
296
|
+
}
|
|
586
297
|
```
|
|
587
298
|
|
|
588
|
-
|
|
299
|
+
- `.offerCodeRedemption(isPresented:onCompletion:)` (iOS 16) presents the
|
|
300
|
+
system sheet; refresh entitlements on success, handle the error on failure.
|
|
301
|
+
- On a subscription store, `.storeButton(.visible, for: .redeemCode)` adds a
|
|
302
|
+
redeem button instead.
|
|
303
|
+
- Test in a StoreKit configuration file and in sandbox. In StoreKit testing,
|
|
304
|
+
`.codeOffer(referenceName:)` from `StoreKitTest` applies a code by its
|
|
305
|
+
reference name. Also drive the real redemption path (button or sheet), then
|
|
306
|
+
check `transaction.offer?.type == .code` and the expected
|
|
307
|
+
`transaction.offer?.id`.
|
|
589
308
|
|
|
590
|
-
|
|
591
|
-
Always check `revocationDate` when evaluating entitlements.
|
|
309
|
+
## Which Offer a Transaction Used
|
|
592
310
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
let percentage = transaction.revocationPercentage
|
|
598
|
-
}
|
|
599
|
-
```
|
|
311
|
+
`transaction.offer` (iOS 17.2) is optional and has `type` and `id`. The
|
|
312
|
+
`type` is `.introductory`, `.promotional`, `.code` or `.winBack`; keep a
|
|
313
|
+
`default` branch for future cases. It replaces `offerType` and `offerID`,
|
|
314
|
+
deprecated in iOS 17.2.
|
|
600
315
|
|
|
601
|
-
##
|
|
316
|
+
## Server-Side Validation
|
|
602
317
|
|
|
603
|
-
|
|
318
|
+
Every StoreKit 2 transaction is a signed JWS. Send the raw string, not decoded
|
|
319
|
+
fields:
|
|
604
320
|
|
|
605
321
|
```swift
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
case .familyShared:
|
|
611
|
-
// Shared via Family Sharing -- may be revoked if sharer leaves
|
|
612
|
-
break
|
|
613
|
-
default:
|
|
614
|
-
break
|
|
322
|
+
func report(_ check: VerificationResult<StoreKit.Transaction>, to api: ReceiptAPI) async throws {
|
|
323
|
+
let transaction = try verified(check)
|
|
324
|
+
try await api.submit(jws: check.jwsRepresentation, product: transaction.productID)
|
|
325
|
+
await transaction.finish()
|
|
615
326
|
}
|
|
616
327
|
```
|
|
617
328
|
|
|
618
|
-
|
|
329
|
+
- On the server, Apple's App Store Server Library decodes and checks it:
|
|
330
|
+
`verifyAndDecodeTransaction(signedTransaction:)` for transactions and
|
|
331
|
+
`verifyAndDecodeRenewalInfo(signedRenewalInfo:)` for renewal info.
|
|
332
|
+
- The format is the `JWSTransaction` used by the App Store Server API and App
|
|
333
|
+
Store Server Notifications V2.
|
|
334
|
+
- Against replay, also send `transaction.deviceVerification` and
|
|
335
|
+
`transaction.deviceVerificationNonce` so the server can bind the
|
|
336
|
+
transaction to the device.
|
|
619
337
|
|
|
620
|
-
|
|
621
|
-
Family Sharing. Enable Family Sharing in App Store Connect per product.
|
|
338
|
+
## Testing in Xcode
|
|
622
339
|
|
|
623
|
-
|
|
624
|
-
member stops sharing, the transaction is revoked.
|
|
340
|
+
Setup:
|
|
625
341
|
|
|
626
|
-
|
|
342
|
+
1. File > New > File > StoreKit Configuration File.
|
|
343
|
+
2. Add products that mirror App Store Connect.
|
|
344
|
+
3. Choose it in Edit Scheme > Run > Options > StoreKit Configuration.
|
|
627
345
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
346
|
+
The file holds product ID, reference name, type, price and locale,
|
|
347
|
+
subscription group and level, introductory and promotional offers, and Family
|
|
348
|
+
Sharing.
|
|
631
349
|
|
|
632
|
-
|
|
633
|
-
case .pending:
|
|
634
|
-
// Show UI indicating purchase needs parental approval
|
|
635
|
-
showPendingApprovalState()
|
|
636
|
-
// When approved, the transaction arrives via Transaction.updates
|
|
637
|
-
```
|
|
350
|
+
Purchase options for tests:
|
|
638
351
|
|
|
639
|
-
|
|
352
|
+
| Option | Source | Effect |
|
|
353
|
+
|--------|--------|--------|
|
|
354
|
+
| `.simulatesAskToBuyInSandbox(true)` | StoreKit | turns the purchase into an Ask to Buy request |
|
|
355
|
+
| `.purchaseDate(someDate, renewalBehavior: .renewUntilNow)` | StoreKitTest, iOS 17 | backdates the purchase; `.cancelImmediately` is the other behaviour |
|
|
356
|
+
| `.codeOffer(referenceName: "SPRING")` | StoreKitTest, iOS 17 | applies an offer code |
|
|
640
357
|
|
|
641
|
-
|
|
642
|
-
try await product.purchase(options: [.simulatesAskToBuyInSandbox(true)])
|
|
643
|
-
```
|
|
644
|
-
|
|
645
|
-
Then approve or decline in Xcode's Transaction Manager. Include this path when
|
|
646
|
-
testing promotional offers, win-back offers, offer codes, and renewal states so
|
|
647
|
-
deferred approval does not bypass verification or entitlement updates.
|
|
358
|
+
What the local test environment lets you do:
|
|
648
359
|
|
|
649
|
-
|
|
360
|
+
- make transactions fail, interrupt purchases, issue refunds;
|
|
361
|
+
- leave Ask to Buy pending for promotional, win-back and offer-code purchases;
|
|
362
|
+
- speed renewals up to minutes instead of months;
|
|
363
|
+
- enter grace period and billing retry;
|
|
364
|
+
- clear purchase history between runs;
|
|
365
|
+
- redeem offers;
|
|
366
|
+
- expire or cancel subscriptions.
|
|
650
367
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
.currentEntitlementTask(for: "com.app.premium") { state in
|
|
655
|
-
self.entitlementState = state
|
|
656
|
-
}
|
|
657
|
-
```
|
|
368
|
+
The Transaction Manager (Debug > StoreKit > Manage Transactions) lists test
|
|
369
|
+
transactions and can delete them (to reset), refund them, expire
|
|
370
|
+
subscriptions, and approve or decline Ask to Buy.
|
|
658
371
|
|
|
659
|
-
|
|
372
|
+
## Renewal States and What to Grant
|
|
660
373
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
374
|
+
| State | Access | Tell the user |
|
|
375
|
+
|-------|--------|---------------|
|
|
376
|
+
| `.subscribed` | full | nothing |
|
|
377
|
+
| `.inGracePeriod` | full | a gentle prompt to update the payment method |
|
|
378
|
+
| `.inBillingRetryPeriod` | your call; Apple suggests limited access to encourage a fix | a "payment failed" banner |
|
|
379
|
+
| `.expired` | none | depends on `renewalInfo.expirationReason` |
|
|
380
|
+
| `.revoked` | none, Apple refunded it | nothing to sell |
|
|
668
381
|
|
|
669
|
-
|
|
382
|
+
Expiration reasons and a reasonable response:
|
|
670
383
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
```
|
|
384
|
+
| `expirationReason` | Meaning | Response |
|
|
385
|
+
|--------------------|---------|----------|
|
|
386
|
+
| `.autoRenewDisabled` | the user cancelled | a resubscribe offer |
|
|
387
|
+
| `.billingError` | payment failed | ask for a new payment method |
|
|
388
|
+
| `.didNotConsentToPriceIncrease` | declined a price rise | explain the new price |
|
|
389
|
+
| `.productUnavailable` | the plan is no longer sold | nothing |
|
|
390
|
+
| `.unknown` | no reason given | nothing specific |
|
|
681
391
|
|
|
682
|
-
|
|
392
|
+
## Grace Period and Billing Retry
|
|
683
393
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
.
|
|
687
|
-
|
|
688
|
-
|
|
394
|
+
- Grace period is switched on in App Store Connect; subscribers keep access
|
|
395
|
+
for a short time after a failed charge. Detect it with
|
|
396
|
+
`status.state == .inGracePeriod` and read
|
|
397
|
+
`renewalInfo.gracePeriodExpirationDate` from the verified renewal info.
|
|
398
|
+
Grant access and ask for a payment update.
|
|
399
|
+
- Apple retries failed charges by itself. The state is
|
|
400
|
+
`.inBillingRetryPeriod`, and `renewalInfo.isInBillingRetry` is `true`.
|
|
689
401
|
|
|
690
|
-
|
|
691
|
-
.storeProductsTask(for: ["id1", "id2"]) { taskState in
|
|
692
|
-
// taskState: Product.CollectionTaskState (.loading, .success([Product], unavailable:), .failure)
|
|
693
|
-
}
|
|
402
|
+
## Refunds
|
|
694
403
|
|
|
695
|
-
|
|
696
|
-
.
|
|
697
|
-
|
|
698
|
-
}
|
|
699
|
-
```
|
|
404
|
+
- From a transaction: `try await transaction.beginRefundRequest(in: windowScene)`.
|
|
405
|
+
- From an ID: `try await Transaction.beginRefundRequest(for: transactionID, in: windowScene)`.
|
|
406
|
+
- In SwiftUI: `.refundRequestSheet(for: transactionID, isPresented: $asking) { result in ... }`.
|
|
700
407
|
|
|
701
|
-
|
|
408
|
+
A granted refund arrives on `Transaction.updates` with a `revocationDate`.
|
|
409
|
+
Remove access for `transaction.productID`; `transaction.revocationPercentage`
|
|
410
|
+
tells a partial refund from a full one.
|
|
702
411
|
|
|
703
|
-
|
|
412
|
+
## Family Sharing
|
|
704
413
|
|
|
705
|
-
|
|
706
|
-
|
|
414
|
+
- `transaction.ownershipType` is `.purchased` (bought by this user) or
|
|
415
|
+
`.familyShared` (can disappear when the organiser stops sharing); keep a
|
|
416
|
+
default branch.
|
|
417
|
+
- `product.isFamilyShareable` says whether the product can be shared; sharing
|
|
418
|
+
is enabled per product in App Store Connect.
|
|
419
|
+
- Sharing changes come through `Transaction.updates`; when a member stops
|
|
420
|
+
sharing, the transaction is revoked.
|
|
421
|
+
|
|
422
|
+
## Ask to Buy
|
|
423
|
+
|
|
424
|
+
- Applies to child accounts in a Family Sharing group.
|
|
425
|
+
- The purchase returns `.pending`. Show a waiting state, keep content locked,
|
|
426
|
+
and unlock only when the approved transaction arrives on
|
|
427
|
+
`Transaction.updates`.
|
|
428
|
+
- Test with `.simulatesAskToBuyInSandbox(true)` and approve or decline in the
|
|
429
|
+
Transaction Manager.
|
|
430
|
+
- Run the Ask to Buy path through promotional, win-back, offer-code and
|
|
431
|
+
renewal tests too, so a deferred approval cannot skip verification or the
|
|
432
|
+
entitlement refresh.
|
|
433
|
+
|
|
434
|
+
## Task Modifiers
|
|
435
|
+
|
|
436
|
+
- `.currentEntitlementTask(for: productID) { state in ... }` (iOS 17) reports
|
|
437
|
+
an `EntitlementTaskState<VerificationResult<Transaction>?>`: `.loading`,
|
|
438
|
+
`.success(value)` or `.failure(any Error)`. Grant access only for
|
|
439
|
+
`.success(.some(.verified(let transaction)))` with a `nil`
|
|
440
|
+
`revocationDate`.
|
|
441
|
+
- `.storeProductTask(for: id) { state in ... }` loads one product;
|
|
442
|
+
`Product.TaskState` is `.loading`, `.success(Product)`, `.unavailable` or
|
|
443
|
+
`.failure`.
|
|
444
|
+
- `.storeProductsTask(for: ids) { state in ... }` loads several;
|
|
445
|
+
`Product.CollectionTaskState` is `.loading`,
|
|
446
|
+
`.success([Product], unavailable:)` or `.failure`.
|
|
447
|
+
- `.subscriptionStatusTask(for: groupID) { state in ... }` follows a group's
|
|
448
|
+
status.
|
|
449
|
+
|
|
450
|
+
## Live Subscription Status
|
|
451
|
+
|
|
452
|
+
`Product.SubscriptionInfo.Status.all` (iOS 17) streams status changes for
|
|
453
|
+
every group:
|
|
454
|
+
|
|
455
|
+
```swift
|
|
456
|
+
func watchPlans(onChange: @escaping @Sendable (Product.SubscriptionInfo.RenewalState) -> Void) -> Task<Void, Never> {
|
|
707
457
|
Task {
|
|
708
|
-
for await (
|
|
458
|
+
for await (_, statuses) in Product.SubscriptionInfo.Status.all {
|
|
709
459
|
for status in statuses {
|
|
710
|
-
guard case .verified
|
|
711
|
-
|
|
460
|
+
guard case .verified = status.renewalInfo else { continue }
|
|
461
|
+
onChange(status.state)
|
|
712
462
|
}
|
|
713
463
|
}
|
|
714
464
|
}
|
|
715
465
|
}
|
|
716
466
|
```
|
|
717
467
|
|
|
718
|
-
## Product
|
|
468
|
+
## Promoted Purchases on the Product Page
|
|
719
469
|
|
|
720
|
-
|
|
721
|
-
product
|
|
470
|
+
`Product.PromotionInfo` (iOS 16.4) reads and overrides, on this device, the
|
|
471
|
+
order and visibility of in-app purchases promoted on the App Store product
|
|
472
|
+
page. Configure the defaults in App Store Connect and override per device only
|
|
473
|
+
when needed.
|
|
722
474
|
|
|
723
|
-
|
|
724
|
-
// Product.PromotionInfo provides device-level promotion customization
|
|
725
|
-
// Configure in App Store Connect and override per-device as needed
|
|
726
|
-
```
|
|
475
|
+
## Price Increases
|
|
727
476
|
|
|
728
|
-
|
|
477
|
+
Read `renewalInfo.priceIncreaseStatus` from verified renewal info:
|
|
729
478
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
// User has not yet consented -- show price increase info
|
|
736
|
-
showPriceIncreaseConsent(newPrice: renewalInfo.renewalPrice,
|
|
737
|
-
currency: renewalInfo.currency)
|
|
738
|
-
case .agreed:
|
|
739
|
-
// User accepted the price increase
|
|
740
|
-
break
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
```
|
|
479
|
+
| Value | Meaning |
|
|
480
|
+
|-------|---------|
|
|
481
|
+
| `.noIncreasePending` | nothing to show |
|
|
482
|
+
| `.pending` | the user has not agreed yet; show the new price from `renewalInfo.renewalPrice` and `renewalInfo.currency` |
|
|
483
|
+
| `.agreed` | accepted |
|
|
744
484
|
|
|
745
485
|
## Unfinished Transactions
|
|
746
486
|
|
|
747
|
-
`Transaction.updates`
|
|
748
|
-
launch.
|
|
749
|
-
|
|
487
|
+
`Transaction.updates` hands over unfinished transactions once, just after
|
|
488
|
+
launch. `Transaction.unfinished` lets you sweep for them on purpose, for
|
|
489
|
+
example after your delivery server was down or when the listener started
|
|
490
|
+
late:
|
|
750
491
|
|
|
751
492
|
```swift
|
|
752
|
-
func
|
|
753
|
-
for await
|
|
754
|
-
guard case .verified(let transaction) =
|
|
755
|
-
await
|
|
493
|
+
func sweepUnfinished(using store: EntitlementStore) async {
|
|
494
|
+
for await pending in StoreKit.Transaction.unfinished {
|
|
495
|
+
guard case .verified(let transaction) = pending else { continue }
|
|
496
|
+
try? await store.deliver(transaction)
|
|
756
497
|
await transaction.finish()
|
|
757
498
|
}
|
|
758
499
|
}
|
|
759
500
|
```
|
|
760
501
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
## Common Advanced Mistakes
|
|
765
|
-
|
|
766
|
-
### Missing a recovery path for unfinished transactions
|
|
767
|
-
|
|
768
|
-
```swift
|
|
769
|
-
// WRONG: No launch listener and no recovery sweep
|
|
770
|
-
init() { }
|
|
771
|
-
|
|
772
|
-
// CORRECT: Start updates at launch; sweep unfinished transactions when needed
|
|
773
|
-
init() {
|
|
774
|
-
transactionListener = listenForTransactions()
|
|
775
|
-
Task { await processUnfinishedTransactions() }
|
|
776
|
-
}
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
### Treating billing retry as expired
|
|
502
|
+
The listener remains the always-on path; the sweep is recovery, not a
|
|
503
|
+
substitute.
|
|
780
504
|
|
|
781
|
-
|
|
782
|
-
// WRONG: Revoking access during billing retry
|
|
783
|
-
case .inBillingRetryPeriod: revokeAccess()
|
|
784
|
-
|
|
785
|
-
// CORRECT: Grant limited access and prompt payment update
|
|
786
|
-
case .inBillingRetryPeriod:
|
|
787
|
-
grantLimitedAccess()
|
|
788
|
-
showUpdatePaymentPrompt()
|
|
789
|
-
```
|
|
505
|
+
## Advanced Mistakes
|
|
790
506
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
grantPermanentAccess()
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
// CORRECT: Check revocation status and listen for changes
|
|
800
|
-
if transaction.ownershipType == .familyShared,
|
|
801
|
-
transaction.revocationDate == nil {
|
|
802
|
-
grantAccess() // May be revoked later via Transaction.updates
|
|
803
|
-
}
|
|
804
|
-
```
|
|
507
|
+
| Wrong | Right |
|
|
508
|
+
|-------|-------|
|
|
509
|
+
| An `App` initializer with neither listener nor sweep | start the listener in `init()`, and run the unfinished sweep in a `Task` when recovery is needed |
|
|
510
|
+
| Removing access in `.inBillingRetryPeriod` | limited access plus a prompt to fix payment |
|
|
511
|
+
| Treating `.familyShared` as permanent | also require `revocationDate == nil` and let `Transaction.updates` deliver a later revocation |
|