@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +285 -285
- package/package.json +3 -3
- package/pipeline/lib/redact.mjs +3 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/scripts/gen-skills-index.mjs +13 -1
- package/pipeline/scripts/pre-commit-check.sh +4 -0
- package/pipeline/skills/.skill-manifest.json +69 -69
- 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 +345 -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 +107 -121
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -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 +335 -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 +277 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -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 +274 -384
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
- 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 +228 -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 +302 -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 +236 -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/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -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/skill-creator/template.md +7 -1
- 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 +302 -241
- 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 +304 -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 +183 -162
- 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 +411 -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 +375 -491
- 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 +397 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -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 +201 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
- 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 +160 -315
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,371 +1,432 @@
|
|
|
1
1
|
# StoreKit 2 Core Patterns
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
SKILL.md keeps the review guidance and short snippets. For control styles,
|
|
7
|
-
offers, testing, server validation, and advanced subscription handling see
|
|
3
|
+
Complete, copyable code for the core flows, followed by the ten common
|
|
4
|
+
mistakes with a wrong and a right version each. Control styles, offers,
|
|
5
|
+
testing, server validation and subscription edge cases are in
|
|
8
6
|
[storekit-advanced.md](storekit-advanced.md).
|
|
9
7
|
|
|
8
|
+
The examples share one entitlement store, one verification helper and one
|
|
9
|
+
completion handler. Files that also import SwiftUI spell the type
|
|
10
|
+
`StoreKit.Transaction`.
|
|
11
|
+
|
|
10
12
|
## Contents
|
|
11
13
|
|
|
12
|
-
- [
|
|
13
|
-
- [Transaction
|
|
14
|
-
- [
|
|
15
|
-
- [
|
|
16
|
-
- [StoreView and ProductView](#storeview-and-productview)
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
14
|
+
- [Buying with PurchaseAction](#buying-with-purchaseaction)
|
|
15
|
+
- [Launch-Time Transaction Listener](#launch-time-transaction-listener)
|
|
16
|
+
- [Computing Entitlements](#computing-entitlements)
|
|
17
|
+
- [Subscription Paywall](#subscription-paywall)
|
|
18
|
+
- [Product Shelves: StoreView and ProductView](#product-shelves-storeview-and-productview)
|
|
19
|
+
- [Is a Plan Active?](#is-a-plan-active)
|
|
20
|
+
- [Verifying the Install with AppTransaction](#verifying-the-install-with-apptransaction)
|
|
21
|
+
- [Store View Callbacks](#store-view-callbacks)
|
|
22
|
+
- [Ten Mistakes, Fixed](#ten-mistakes-fixed)
|
|
21
23
|
|
|
22
|
-
##
|
|
24
|
+
## Buying with PurchaseAction
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
finish.
|
|
26
|
+
A custom SwiftUI button that buys through `PurchaseAction` (iOS 17):
|
|
26
27
|
|
|
27
28
|
```swift
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
])
|
|
34
|
-
switch result {
|
|
35
|
-
case .success(let verification):
|
|
36
|
-
let transaction = try checkVerified(verification)
|
|
37
|
-
await deliverContent(for: transaction)
|
|
38
|
-
await transaction.finish()
|
|
39
|
-
case .userCancelled:
|
|
40
|
-
break
|
|
41
|
-
case .pending:
|
|
42
|
-
// Ask to Buy or deferred approval: show pending UI, no unlock yet.
|
|
43
|
-
showPendingApprovalMessage()
|
|
44
|
-
@unknown default:
|
|
45
|
-
break
|
|
46
|
-
}
|
|
29
|
+
import StoreKit
|
|
30
|
+
import SwiftUI
|
|
31
|
+
|
|
32
|
+
func verified<Value>(_ check: VerificationResult<Value>) throws -> Value {
|
|
33
|
+
try check.payloadValue
|
|
47
34
|
}
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
struct BuyProButton: View {
|
|
37
|
+
let product: Product
|
|
38
|
+
let accountID: UUID
|
|
39
|
+
@Environment(\.purchase) private var purchase
|
|
40
|
+
@State private var awaitingApproval = false
|
|
41
|
+
|
|
42
|
+
var body: some View {
|
|
43
|
+
Button("Unlock for \(product.displayPrice)") {
|
|
44
|
+
Task { await buy() }
|
|
45
|
+
}
|
|
46
|
+
.alert("Waiting for approval", isPresented: $awaitingApproval) {}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private func buy() async {
|
|
50
|
+
let options: Set<Product.PurchaseOption> = [.appAccountToken(accountID)]
|
|
51
|
+
do {
|
|
52
|
+
switch try await purchase(product, options: options) {
|
|
53
|
+
case .pending:
|
|
54
|
+
awaitingApproval = true
|
|
55
|
+
case .userCancelled:
|
|
56
|
+
return
|
|
57
|
+
case .success(let check):
|
|
58
|
+
let receipt = try verified(check)
|
|
59
|
+
try await EntitlementStore.shared.deliver(receipt)
|
|
60
|
+
await receipt.finish()
|
|
61
|
+
@unknown default:
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
awaitingApproval = false
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
}
|
|
55
69
|
```
|
|
56
70
|
|
|
57
|
-
|
|
71
|
+
- `.success` carries a `VerificationResult`. `verified(_:)` reads its
|
|
72
|
+
`payloadValue`, which returns the transaction when the signature checks out
|
|
73
|
+
and throws the verification error when it does not. Deliver, then finish.
|
|
74
|
+
- `.userCancelled` does nothing.
|
|
75
|
+
- `.pending` means Ask to Buy or another deferred approval: tell the user and
|
|
76
|
+
unlock nothing until the approved transaction arrives on
|
|
77
|
+
`Transaction.updates`.
|
|
78
|
+
- `@unknown default` keeps the switch valid when Apple adds a case.
|
|
58
79
|
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
## Launch-Time Transaction Listener
|
|
81
|
+
|
|
82
|
+
Start it in the `App` initializer and keep the task for the life of the
|
|
83
|
+
process. Its type is `Task<Void, Never>`, so no thrown error can end it.
|
|
61
84
|
|
|
62
85
|
```swift
|
|
86
|
+
import StoreKit
|
|
87
|
+
import SwiftUI
|
|
88
|
+
|
|
63
89
|
@main
|
|
64
|
-
struct
|
|
65
|
-
private let
|
|
90
|
+
struct RecipesApp: App {
|
|
91
|
+
private let purchaseWatcher: Task<Void, Never>
|
|
66
92
|
|
|
67
93
|
init() {
|
|
68
|
-
|
|
94
|
+
purchaseWatcher = Self.watchTransactions()
|
|
69
95
|
}
|
|
70
96
|
|
|
71
97
|
var body: some Scene {
|
|
72
|
-
WindowGroup {
|
|
98
|
+
WindowGroup { Text("Recipes") }
|
|
73
99
|
}
|
|
74
100
|
|
|
75
|
-
static func
|
|
101
|
+
private static func watchTransactions() -> Task<Void, Never> {
|
|
76
102
|
Task(priority: .background) {
|
|
77
|
-
for await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
for await incoming in StoreKit.Transaction.updates {
|
|
104
|
+
if case .verified(let receipt) = incoming {
|
|
105
|
+
await EntitlementStore.shared.refresh()
|
|
106
|
+
await receipt.finish()
|
|
107
|
+
}
|
|
81
108
|
}
|
|
82
109
|
}
|
|
83
110
|
}
|
|
84
111
|
}
|
|
85
112
|
```
|
|
86
113
|
|
|
87
|
-
|
|
114
|
+
Unverified updates fall through untouched. For each verified one,
|
|
115
|
+
entitlements are recomputed first and the transaction finished afterwards.
|
|
116
|
+
|
|
117
|
+
A plain `Task` is enough here. It inherits the main actor from the `App`, but
|
|
118
|
+
the loop spends its time suspended in `for await` and the entitlement refresh
|
|
119
|
+
runs on the main-actor `EntitlementStore` anyway; `Task.detached` would only add
|
|
120
|
+
a hop and drop the task-local values.
|
|
88
121
|
|
|
89
|
-
|
|
90
|
-
`revocationDate`. Track consumable fulfillment in your own app or server ledger.
|
|
122
|
+
## Computing Entitlements
|
|
91
123
|
|
|
92
124
|
```swift
|
|
93
|
-
|
|
125
|
+
import Observation
|
|
126
|
+
import StoreKit
|
|
127
|
+
|
|
94
128
|
@MainActor
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
129
|
+
@Observable
|
|
130
|
+
final class EntitlementStore {
|
|
131
|
+
static let shared = EntitlementStore()
|
|
132
|
+
|
|
133
|
+
private(set) var owned: Set<String> = []
|
|
134
|
+
|
|
135
|
+
var hasPro: Bool { owned.contains(StoreItem.proUnlock) }
|
|
136
|
+
|
|
137
|
+
func refresh() async {
|
|
138
|
+
var active: Set<String> = []
|
|
139
|
+
for await entry in StoreKit.Transaction.currentEntitlements {
|
|
140
|
+
guard case .verified(let receipt) = entry,
|
|
141
|
+
receipt.revocationDate == nil else { continue }
|
|
142
|
+
active.insert(receipt.productID)
|
|
107
143
|
}
|
|
108
|
-
|
|
144
|
+
owned = active
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func deliver(_ receipt: StoreKit.Transaction) async throws {
|
|
148
|
+
await refresh()
|
|
109
149
|
}
|
|
110
150
|
}
|
|
151
|
+
|
|
152
|
+
func settle(_ outcome: Result<Product.PurchaseResult, any Error>) async {
|
|
153
|
+
guard case .success(.success(let check)) = outcome,
|
|
154
|
+
let receipt = try? verified(check) else { return }
|
|
155
|
+
try? await EntitlementStore.shared.deliver(receipt)
|
|
156
|
+
await receipt.finish()
|
|
157
|
+
}
|
|
111
158
|
```
|
|
112
159
|
|
|
160
|
+
`refresh()` starts from an empty set each time: only verified entries with no
|
|
161
|
+
`revocationDate` count, and the result replaces the old set in one
|
|
162
|
+
assignment. `settle(_:)` is the shared completion handler for store views:
|
|
163
|
+
the outer `Result` says whether the purchase call failed, the inner
|
|
164
|
+
`PurchaseResult` whether it succeeded, and the `VerificationResult` whether it
|
|
165
|
+
can be trusted.
|
|
166
|
+
|
|
113
167
|
### SwiftUI .currentEntitlementTask Modifier
|
|
114
168
|
|
|
169
|
+
For a single product, a view can gate itself (iOS 17):
|
|
170
|
+
|
|
115
171
|
```swift
|
|
116
|
-
|
|
117
|
-
|
|
172
|
+
import StoreKit
|
|
173
|
+
import SwiftUI
|
|
174
|
+
|
|
175
|
+
struct ProGate<Unlocked: View, Locked: View>: View {
|
|
176
|
+
let productID: String
|
|
177
|
+
@ViewBuilder var unlocked: Unlocked
|
|
178
|
+
@ViewBuilder var locked: Locked
|
|
179
|
+
@State private var gate: EntitlementTaskState<VerificationResult<StoreKit.Transaction>?> = .loading
|
|
118
180
|
|
|
119
181
|
var body: some View {
|
|
120
|
-
|
|
121
|
-
switch
|
|
122
|
-
case .loading:
|
|
123
|
-
|
|
124
|
-
case .
|
|
125
|
-
|
|
182
|
+
ZStack {
|
|
183
|
+
switch gate {
|
|
184
|
+
case .loading:
|
|
185
|
+
ProgressView()
|
|
186
|
+
case .failure:
|
|
187
|
+
locked
|
|
188
|
+
case .success(.some(.verified(let receipt))) where receipt.revocationDate == nil:
|
|
189
|
+
unlocked
|
|
126
190
|
case .success:
|
|
127
|
-
|
|
191
|
+
locked
|
|
192
|
+
@unknown default:
|
|
193
|
+
locked
|
|
128
194
|
}
|
|
129
195
|
}
|
|
130
|
-
.currentEntitlementTask(for:
|
|
131
|
-
self.state = state
|
|
132
|
-
}
|
|
196
|
+
.currentEntitlementTask(for: productID) { gate = $0 }
|
|
133
197
|
}
|
|
134
198
|
}
|
|
135
199
|
```
|
|
136
200
|
|
|
137
|
-
##
|
|
201
|
+
## Subscription Paywall
|
|
138
202
|
|
|
139
|
-
|
|
140
|
-
|
|
203
|
+
The built-in view loads the group's products, shows the purchase UI and can
|
|
204
|
+
restore by itself.
|
|
141
205
|
|
|
142
206
|
```swift
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
207
|
+
import StoreKit
|
|
208
|
+
import SwiftUI
|
|
209
|
+
|
|
210
|
+
struct MembershipPaywall: View {
|
|
211
|
+
let group: String
|
|
212
|
+
let terms: URL
|
|
213
|
+
let privacy: URL
|
|
214
|
+
|
|
215
|
+
var body: some View {
|
|
216
|
+
SubscriptionStoreView(groupID: group)
|
|
217
|
+
.storeButton(.visible, for: .restorePurchases, .redeemCode)
|
|
218
|
+
.subscriptionStorePolicyDestination(url: terms, for: .termsOfService)
|
|
219
|
+
.subscriptionStorePolicyDestination(url: privacy, for: .privacyPolicy)
|
|
220
|
+
.subscriptionStoreButtonLabel(.multiline)
|
|
221
|
+
.subscriptionStoreControlStyle(.prominentPicker)
|
|
222
|
+
.onInAppPurchaseCompletion { _, outcome in await settle(outcome) }
|
|
155
223
|
}
|
|
224
|
+
}
|
|
156
225
|
```
|
|
157
226
|
|
|
158
227
|
### Custom Marketing Content
|
|
159
228
|
|
|
229
|
+
A trailing closure replaces the header; `.containerBackground(_:for:)` with
|
|
230
|
+
`.subscriptionStore` paints behind the whole paywall.
|
|
231
|
+
|
|
160
232
|
```swift
|
|
161
|
-
SubscriptionStoreView(groupID:
|
|
162
|
-
VStack {
|
|
163
|
-
Image(systemName: "
|
|
164
|
-
|
|
165
|
-
Text("
|
|
233
|
+
SubscriptionStoreView(groupID: group) {
|
|
234
|
+
VStack(spacing: 8) {
|
|
235
|
+
Image(systemName: "fork.knife.circle.fill")
|
|
236
|
+
.font(.system(size: 56))
|
|
237
|
+
Text("Recipes Plus").font(.largeTitle.bold())
|
|
238
|
+
Text("Every cookbook, offline, on all your devices")
|
|
239
|
+
.foregroundStyle(.secondary)
|
|
166
240
|
}
|
|
241
|
+
.padding()
|
|
167
242
|
}
|
|
168
|
-
.containerBackground(.
|
|
243
|
+
.containerBackground(.orange.gradient, for: .subscriptionStore)
|
|
169
244
|
```
|
|
170
245
|
|
|
171
|
-
|
|
172
|
-
`SubscriptionPeriodGroupSet`) and control styles
|
|
246
|
+
Grouped layouts (`SubscriptionOptionGroup`, `SubscriptionOptionSection`,
|
|
247
|
+
`SubscriptionPeriodGroupSet`) and the control styles are in
|
|
173
248
|
[storekit-advanced.md](storekit-advanced.md).
|
|
174
249
|
|
|
175
|
-
## StoreView and ProductView
|
|
250
|
+
## Product Shelves: StoreView and ProductView
|
|
176
251
|
|
|
177
|
-
|
|
252
|
+
`StoreView` lists several products, each with its localized name, price and
|
|
253
|
+
buy button; `ProductView` shows one. `.productViewStyle(.large)` works on
|
|
254
|
+
both. The icon closure that receives a `ProductIconPhase` exists on
|
|
255
|
+
`ProductView(id:icon:placeholderIcon:)`, so an ID-based view with a phase
|
|
256
|
+
closure also needs `placeholderIcon`; `ProductView(_ product:icon:)` takes the
|
|
257
|
+
phase closure alone.
|
|
178
258
|
|
|
179
259
|
```swift
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
260
|
+
import StoreKit
|
|
261
|
+
import SwiftUI
|
|
262
|
+
|
|
263
|
+
struct AddOnShelf: View {
|
|
264
|
+
let ids: [String]
|
|
265
|
+
|
|
266
|
+
var body: some View {
|
|
267
|
+
StoreView(ids: ids, prefersPromotionalIcon: true)
|
|
268
|
+
.storeButton(.visible, for: .restorePurchases)
|
|
269
|
+
.productViewStyle(.large)
|
|
270
|
+
.onInAppPurchaseCompletion { _, outcome in await settle(outcome) }
|
|
188
271
|
}
|
|
189
|
-
|
|
272
|
+
}
|
|
190
273
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
274
|
+
struct FeaturedPack: View {
|
|
275
|
+
let id: String
|
|
276
|
+
|
|
277
|
+
var body: some View {
|
|
278
|
+
ProductView(id: id) { phase in
|
|
279
|
+
if case .success(let art) = phase {
|
|
280
|
+
art.resizable().aspectRatio(contentMode: .fit)
|
|
281
|
+
} else if case .loading = phase {
|
|
282
|
+
ProgressView()
|
|
283
|
+
} else {
|
|
284
|
+
Image(systemName: "shippingbox")
|
|
285
|
+
}
|
|
286
|
+
} placeholderIcon: {
|
|
287
|
+
ProgressView()
|
|
288
|
+
}
|
|
289
|
+
.productViewStyle(.large)
|
|
197
290
|
}
|
|
198
291
|
}
|
|
199
|
-
.productViewStyle(.large)
|
|
200
292
|
```
|
|
201
293
|
|
|
202
|
-
##
|
|
294
|
+
## Is a Plan Active?
|
|
203
295
|
|
|
204
296
|
```swift
|
|
205
|
-
func
|
|
206
|
-
let statuses = try await Product.SubscriptionInfo.status(for:
|
|
207
|
-
|
|
208
|
-
guard case .verified =
|
|
209
|
-
case .verified =
|
|
210
|
-
|
|
211
|
-
return true
|
|
212
|
-
}
|
|
297
|
+
func hasActivePlan(in group: String) async throws -> Bool {
|
|
298
|
+
let statuses = try await Product.SubscriptionInfo.status(for: group)
|
|
299
|
+
return statuses.contains { entry in
|
|
300
|
+
guard case .verified = entry.transaction,
|
|
301
|
+
case .verified = entry.renewalInfo else { return false }
|
|
302
|
+
return [.subscribed, .inGracePeriod].contains(entry.state)
|
|
213
303
|
}
|
|
214
|
-
return false
|
|
215
304
|
}
|
|
216
305
|
```
|
|
217
306
|
|
|
218
|
-
|
|
307
|
+
A status counts only when both its renewal info and its transaction verify.
|
|
219
308
|
|
|
220
|
-
|
|
221
|
-
or detecting tampered installations (iOS 16+).
|
|
309
|
+
## Verifying the Install with AppTransaction
|
|
222
310
|
|
|
223
311
|
```swift
|
|
224
|
-
func
|
|
312
|
+
func checkInstall() async -> Date? {
|
|
225
313
|
do {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
let purchaseDate = appTransaction.originalPurchaseDate
|
|
231
|
-
// Migration logic for users who paid before subscription model
|
|
314
|
+
switch try await AppTransaction.shared {
|
|
315
|
+
case .verified(let install):
|
|
316
|
+
_ = install.originalAppVersion
|
|
317
|
+
return install.originalPurchaseDate
|
|
232
318
|
case .unverified:
|
|
233
|
-
|
|
234
|
-
break
|
|
319
|
+
return nil
|
|
235
320
|
}
|
|
236
|
-
} catch {
|
|
321
|
+
} catch {
|
|
322
|
+
return nil
|
|
323
|
+
}
|
|
237
324
|
}
|
|
238
325
|
```
|
|
239
326
|
|
|
240
|
-
|
|
327
|
+
With `.verified`, `originalAppVersion` and `originalPurchaseDate` tell you
|
|
328
|
+
whether this user bought the app before a switch to subscriptions, so you can
|
|
329
|
+
grandfather them. `.unverified` may mean a tampered install: restrict
|
|
330
|
+
features. The `catch` covers failing to fetch the app transaction at all.
|
|
331
|
+
|
|
332
|
+
## Store View Callbacks
|
|
241
333
|
|
|
242
334
|
```swift
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
335
|
+
struct TrackedStore: View {
|
|
336
|
+
let ids: [String]
|
|
337
|
+
let accountID: UUID
|
|
338
|
+
let log: @Sendable (String) -> Void
|
|
339
|
+
|
|
340
|
+
var body: some View {
|
|
341
|
+
StoreView(ids: ids)
|
|
342
|
+
.inAppPurchaseOptions { _ in [.appAccountToken(accountID)] }
|
|
343
|
+
.onInAppPurchaseStart { product in log("purchase_started \(product.id)") }
|
|
344
|
+
.onInAppPurchaseCompletion { _, outcome in await settle(outcome) }
|
|
250
345
|
}
|
|
251
346
|
}
|
|
252
|
-
.inAppPurchaseOptions { product in
|
|
253
|
-
[.appAccountToken(userAccountToken)]
|
|
254
|
-
}
|
|
255
347
|
```
|
|
256
348
|
|
|
257
|
-
|
|
349
|
+
`.onInAppPurchaseStart` suits analytics, `.onInAppPurchaseCompletion` does
|
|
350
|
+
verify, deliver, finish, and `.inAppPurchaseOptions` supplies options per
|
|
351
|
+
product.
|
|
258
352
|
|
|
259
|
-
|
|
353
|
+
## Ten Mistakes, Fixed
|
|
260
354
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
355
|
+
### 1. No listener at launch
|
|
356
|
+
|
|
357
|
+
Wrong: an `App` with an empty initializer never hears about renewals, refunds
|
|
358
|
+
or Ask to Buy approvals.
|
|
359
|
+
|
|
360
|
+
Right: start `Transaction.updates` in `init()`, as in
|
|
361
|
+
[Launch-Time Transaction Listener](#launch-time-transaction-listener).
|
|
362
|
+
|
|
363
|
+
### 2. Never finishing
|
|
364
|
+
|
|
365
|
+
Wrong: unlocking and returning. The transaction comes back in the unfinished
|
|
366
|
+
queue on every launch.
|
|
268
367
|
|
|
269
|
-
|
|
368
|
+
Right: record the delivery with a call that can fail, then finish. If the
|
|
369
|
+
record fails, leave the transaction unfinished so it is offered again.
|
|
270
370
|
|
|
271
371
|
```swift
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
// CORRECT: Deliver durably, then finish. If delivery fails, do not finish yet.
|
|
277
|
-
let transaction = try checkVerified(verification)
|
|
278
|
-
try await recordDelivery(transaction)
|
|
279
|
-
await transaction.finish()
|
|
372
|
+
func complete(_ receipt: StoreKit.Transaction, ledger: PurchaseLedger) async throws {
|
|
373
|
+
try await ledger.record(receipt.id, product: receipt.productID)
|
|
374
|
+
await receipt.finish()
|
|
375
|
+
}
|
|
280
376
|
```
|
|
281
377
|
|
|
282
|
-
### 3.
|
|
378
|
+
### 3. Trusting unverified data
|
|
283
379
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
let transaction = verification.unsafePayloadValue
|
|
380
|
+
Wrong: `check.unsafePayloadValue` hands you the payload whether or not the
|
|
381
|
+
signature holds; that is a security hole.
|
|
287
382
|
|
|
288
|
-
|
|
289
|
-
let transaction = try checkVerified(verification)
|
|
290
|
-
```
|
|
383
|
+
Right: `let receipt = try verified(check)`.
|
|
291
384
|
|
|
292
|
-
### 4.
|
|
385
|
+
### 4. Original IAP API in new code
|
|
293
386
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
|
|
297
|
-
SKPaymentQueue.default().add(payment)
|
|
387
|
+
Avoid: `SKProductsRequest(productIdentifiers:)` and
|
|
388
|
+
`SKPaymentQueue.default().add(payment)`.
|
|
298
389
|
|
|
299
|
-
|
|
300
|
-
let products = try await Product.products(for: ["com.app.premium"])
|
|
301
|
-
let result = try await product.purchase()
|
|
302
|
-
```
|
|
390
|
+
Prefer: `Product.products(for:)` and `product.purchase()`.
|
|
303
391
|
|
|
304
|
-
### 5.
|
|
392
|
+
### 5. Ignoring revocation
|
|
305
393
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if case .verified(let transaction) = result {
|
|
309
|
-
purchased.insert(transaction.productID)
|
|
310
|
-
}
|
|
394
|
+
Wrong: inserting `productID` for every verified transaction also grants
|
|
395
|
+
refunded purchases.
|
|
311
396
|
|
|
312
|
-
|
|
313
|
-
if case .verified(let transaction) = result, transaction.revocationDate == nil {
|
|
314
|
-
purchased.insert(transaction.productID)
|
|
315
|
-
}
|
|
316
|
-
```
|
|
397
|
+
Right: require `revocationDate == nil` as well.
|
|
317
398
|
|
|
318
|
-
### 6.
|
|
399
|
+
### 6. Hard-coded prices
|
|
319
400
|
|
|
320
|
-
|
|
321
|
-
// WRONG: Wrong for other currencies and regions
|
|
322
|
-
Text("Buy Premium for $4.99")
|
|
401
|
+
Wrong: `Text("Pro for $4.99")` is wrong in every other currency and region.
|
|
323
402
|
|
|
324
|
-
|
|
325
|
-
Text("Buy \(product.displayName) for \(product.displayPrice)")
|
|
326
|
-
```
|
|
403
|
+
Right: `Text("\(product.displayName): \(product.displayPrice)")`.
|
|
327
404
|
|
|
328
|
-
### 7.
|
|
405
|
+
### 7. Dropping pending purchases
|
|
329
406
|
|
|
330
|
-
|
|
331
|
-
// WRONG: Silently drops pending Ask to Buy
|
|
332
|
-
default: break
|
|
407
|
+
Wrong: `default: break` in the result switch silently swallows Ask to Buy.
|
|
333
408
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
showPendingApprovalMessage()
|
|
337
|
-
```
|
|
409
|
+
Right: a `case .pending` that tells the user approval is pending; unlock
|
|
410
|
+
only when `Transaction.updates` delivers the approved transaction.
|
|
338
411
|
|
|
339
|
-
### 8.
|
|
412
|
+
### 8. One entitlement check at launch
|
|
340
413
|
|
|
341
|
-
|
|
342
|
-
// WRONG: Check once, never update
|
|
343
|
-
func appDidFinish() { Task { await updateEntitlements() } }
|
|
414
|
+
Wrong: computing entitlements once and caching them for the session.
|
|
344
415
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
```
|
|
416
|
+
Right: recompute from the `Transaction.updates` listener for changes during
|
|
417
|
+
the session, and again when the app returns to the foreground, for example
|
|
418
|
+
`.task { await store.refresh() }` on the content view.
|
|
349
419
|
|
|
350
|
-
### 9.
|
|
420
|
+
### 9. No restore control
|
|
351
421
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
SubscriptionStoreView(groupID: "group_id")
|
|
422
|
+
Wrong: a `SubscriptionStoreView` without restore, which App Review may
|
|
423
|
+
reject.
|
|
355
424
|
|
|
356
|
-
|
|
357
|
-
SubscriptionStoreView(groupID: "group_id")
|
|
358
|
-
.storeButton(.visible, for: .restorePurchases)
|
|
359
|
-
```
|
|
425
|
+
Right: `.storeButton(.visible, for: .restorePurchases)`.
|
|
360
426
|
|
|
361
|
-
### 10.
|
|
427
|
+
### 10. No policy links
|
|
362
428
|
|
|
363
|
-
|
|
364
|
-
// WRONG: No terms or privacy policy
|
|
365
|
-
SubscriptionStoreView(groupID: "group_id")
|
|
429
|
+
Wrong: a subscription screen with no Terms of Service or Privacy Policy.
|
|
366
430
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
|
|
370
|
-
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
|
|
371
|
-
```
|
|
431
|
+
Right: both `.subscriptionStorePolicyDestination(url:for:)` calls, one for
|
|
432
|
+
`.termsOfService` and one for `.privacyPolicy`.
|