@mmerterden/multi-agent-pipeline 20.6.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 +27 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +328 -329
- package/package.json +2 -2
- package/pipeline/multi-agent-refs/features/design-conformance.md +62 -64
- package/pipeline/scripts/_notices.mjs +12 -1
- package/pipeline/scripts/gen-skills-index.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +106 -106
- package/pipeline/skills/shared/README.md +71 -71
- package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +1 -0
- 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/android-architecture/SKILL.md +2 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +2 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +2 -0
- 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/backlog/BACKLOG.md +1 -1
- package/pipeline/skills/shared/external/backlog/SKILL.md +56 -33
- 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/ci-cd-pipelines/SKILL.md +1 -0
- 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/compose-components/SKILL.md +2 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +3 -2
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +2 -0
- 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/council/SKILL.md +1 -0
- 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/css-modern/SKILL.md +1 -0
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -0
- 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/evidence-github/SKILL.md +2 -0
- package/pipeline/skills/shared/external/evidence-registry/SKILL.md +2 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +2 -0
- 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/html-semantic/SKILL.md +1 -0
- package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -0
- 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-coding-standard/SKILL.md +1 -0
- 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-module-structure/SKILL.md +1 -0
- 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-security/SKILL.md +2 -0
- 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 +91 -283
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +119 -151
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +60 -90
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +119 -156
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +726 -787
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +253 -288
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +243 -304
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +88 -104
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +181 -235
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +198 -263
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +461 -466
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +145 -151
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +123 -141
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +146 -157
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +22 -19
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +156 -140
- 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/nextjs-app-router/SKILL.md +1 -0
- 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/play-store-review/SKILL.md +2 -0
- 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/python-patterns/SKILL.md +1 -0
- package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -0
- 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/rest-api-design/SKILL.md +1 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +2 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +2 -0
- package/pipeline/skills/shared/external/search-first/SKILL.md +1 -0
- 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/signal-community/SKILL.md +2 -0
- package/pipeline/skills/shared/external/skill-creator/SKILL.md +80 -41
- package/pipeline/skills/shared/external/skill-creator/audit.md +63 -59
- package/pipeline/skills/shared/external/skill-creator/checklist.md +28 -20
- package/pipeline/skills/shared/external/skill-creator/examples.md +40 -40
- package/pipeline/skills/shared/external/skill-creator/label-check.md +48 -36
- package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +91 -100
- package/pipeline/skills/shared/external/skill-creator/template.md +51 -39
- 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/tailwind-css/SKILL.md +1 -0
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -0
- 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/typescript-patterns/SKILL.md +1 -0
- 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/vue-composition/SKILL.md +1 -0
- 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/web-accessibility/SKILL.md +1 -0
- package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -0
- package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -0
- 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,371 +1,427 @@
|
|
|
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
|
|
76
|
-
Task(priority: .background) {
|
|
77
|
-
for await
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
101
|
+
private static func watchTransactions() -> Task<Void, Never> {
|
|
102
|
+
Task.detached(priority: .background) {
|
|
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.
|
|
88
116
|
|
|
89
|
-
|
|
90
|
-
`revocationDate`. Track consumable fulfillment in your own app or server ledger.
|
|
117
|
+
## Computing Entitlements
|
|
91
118
|
|
|
92
119
|
```swift
|
|
93
|
-
|
|
120
|
+
import Observation
|
|
121
|
+
import StoreKit
|
|
122
|
+
|
|
94
123
|
@MainActor
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
124
|
+
@Observable
|
|
125
|
+
final class EntitlementStore {
|
|
126
|
+
static let shared = EntitlementStore()
|
|
127
|
+
|
|
128
|
+
private(set) var owned: Set<String> = []
|
|
129
|
+
|
|
130
|
+
var hasPro: Bool { owned.contains(StoreItem.proUnlock) }
|
|
131
|
+
|
|
132
|
+
func refresh() async {
|
|
133
|
+
var active: Set<String> = []
|
|
134
|
+
for await entry in StoreKit.Transaction.currentEntitlements {
|
|
135
|
+
guard case .verified(let receipt) = entry,
|
|
136
|
+
receipt.revocationDate == nil else { continue }
|
|
137
|
+
active.insert(receipt.productID)
|
|
107
138
|
}
|
|
108
|
-
|
|
139
|
+
owned = active
|
|
109
140
|
}
|
|
141
|
+
|
|
142
|
+
func deliver(_ receipt: StoreKit.Transaction) async throws {
|
|
143
|
+
await refresh()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
func settle(_ outcome: Result<Product.PurchaseResult, any Error>) async {
|
|
148
|
+
guard case .success(.success(let check)) = outcome,
|
|
149
|
+
let receipt = try? verified(check) else { return }
|
|
150
|
+
try? await EntitlementStore.shared.deliver(receipt)
|
|
151
|
+
await receipt.finish()
|
|
110
152
|
}
|
|
111
153
|
```
|
|
112
154
|
|
|
155
|
+
`refresh()` starts from an empty set each time: only verified entries with no
|
|
156
|
+
`revocationDate` count, and the result replaces the old set in one
|
|
157
|
+
assignment. `settle(_:)` is the shared completion handler for store views:
|
|
158
|
+
the outer `Result` says whether the purchase call failed, the inner
|
|
159
|
+
`PurchaseResult` whether it succeeded, and the `VerificationResult` whether it
|
|
160
|
+
can be trusted.
|
|
161
|
+
|
|
113
162
|
### SwiftUI .currentEntitlementTask Modifier
|
|
114
163
|
|
|
164
|
+
For a single product, a view can gate itself (iOS 17):
|
|
165
|
+
|
|
115
166
|
```swift
|
|
116
|
-
|
|
117
|
-
|
|
167
|
+
import StoreKit
|
|
168
|
+
import SwiftUI
|
|
169
|
+
|
|
170
|
+
struct ProGate<Unlocked: View, Locked: View>: View {
|
|
171
|
+
let productID: String
|
|
172
|
+
@ViewBuilder var unlocked: Unlocked
|
|
173
|
+
@ViewBuilder var locked: Locked
|
|
174
|
+
@State private var gate: EntitlementTaskState<VerificationResult<StoreKit.Transaction>?> = .loading
|
|
118
175
|
|
|
119
176
|
var body: some View {
|
|
120
|
-
|
|
121
|
-
switch
|
|
122
|
-
case .loading:
|
|
123
|
-
|
|
124
|
-
case .
|
|
125
|
-
|
|
177
|
+
ZStack {
|
|
178
|
+
switch gate {
|
|
179
|
+
case .loading:
|
|
180
|
+
ProgressView()
|
|
181
|
+
case .failure:
|
|
182
|
+
locked
|
|
183
|
+
case .success(.some(.verified(let receipt))) where receipt.revocationDate == nil:
|
|
184
|
+
unlocked
|
|
126
185
|
case .success:
|
|
127
|
-
|
|
186
|
+
locked
|
|
187
|
+
@unknown default:
|
|
188
|
+
locked
|
|
128
189
|
}
|
|
129
190
|
}
|
|
130
|
-
.currentEntitlementTask(for:
|
|
131
|
-
self.state = state
|
|
132
|
-
}
|
|
191
|
+
.currentEntitlementTask(for: productID) { gate = $0 }
|
|
133
192
|
}
|
|
134
193
|
}
|
|
135
194
|
```
|
|
136
195
|
|
|
137
|
-
##
|
|
196
|
+
## Subscription Paywall
|
|
138
197
|
|
|
139
|
-
|
|
140
|
-
|
|
198
|
+
The built-in view loads the group's products, shows the purchase UI and can
|
|
199
|
+
restore by itself.
|
|
141
200
|
|
|
142
201
|
```swift
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
202
|
+
import StoreKit
|
|
203
|
+
import SwiftUI
|
|
204
|
+
|
|
205
|
+
struct MembershipPaywall: View {
|
|
206
|
+
let group: String
|
|
207
|
+
let terms: URL
|
|
208
|
+
let privacy: URL
|
|
209
|
+
|
|
210
|
+
var body: some View {
|
|
211
|
+
SubscriptionStoreView(groupID: group)
|
|
212
|
+
.storeButton(.visible, for: .restorePurchases, .redeemCode)
|
|
213
|
+
.subscriptionStorePolicyDestination(url: terms, for: .termsOfService)
|
|
214
|
+
.subscriptionStorePolicyDestination(url: privacy, for: .privacyPolicy)
|
|
215
|
+
.subscriptionStoreButtonLabel(.multiline)
|
|
216
|
+
.subscriptionStoreControlStyle(.prominentPicker)
|
|
217
|
+
.onInAppPurchaseCompletion { _, outcome in await settle(outcome) }
|
|
155
218
|
}
|
|
219
|
+
}
|
|
156
220
|
```
|
|
157
221
|
|
|
158
222
|
### Custom Marketing Content
|
|
159
223
|
|
|
224
|
+
A trailing closure replaces the header; `.containerBackground(_:for:)` with
|
|
225
|
+
`.subscriptionStore` paints behind the whole paywall.
|
|
226
|
+
|
|
160
227
|
```swift
|
|
161
|
-
SubscriptionStoreView(groupID:
|
|
162
|
-
VStack {
|
|
163
|
-
Image(systemName: "
|
|
164
|
-
|
|
165
|
-
Text("
|
|
228
|
+
SubscriptionStoreView(groupID: group) {
|
|
229
|
+
VStack(spacing: 8) {
|
|
230
|
+
Image(systemName: "fork.knife.circle.fill")
|
|
231
|
+
.font(.system(size: 56))
|
|
232
|
+
Text("Recipes Plus").font(.largeTitle.bold())
|
|
233
|
+
Text("Every cookbook, offline, on all your devices")
|
|
234
|
+
.foregroundStyle(.secondary)
|
|
166
235
|
}
|
|
236
|
+
.padding()
|
|
167
237
|
}
|
|
168
|
-
.containerBackground(.
|
|
238
|
+
.containerBackground(.orange.gradient, for: .subscriptionStore)
|
|
169
239
|
```
|
|
170
240
|
|
|
171
|
-
|
|
172
|
-
`SubscriptionPeriodGroupSet`) and control styles
|
|
241
|
+
Grouped layouts (`SubscriptionOptionGroup`, `SubscriptionOptionSection`,
|
|
242
|
+
`SubscriptionPeriodGroupSet`) and the control styles are in
|
|
173
243
|
[storekit-advanced.md](storekit-advanced.md).
|
|
174
244
|
|
|
175
|
-
## StoreView and ProductView
|
|
245
|
+
## Product Shelves: StoreView and ProductView
|
|
176
246
|
|
|
177
|
-
|
|
247
|
+
`StoreView` lists several products, each with its localized name, price and
|
|
248
|
+
buy button; `ProductView` shows one. `.productViewStyle(.large)` works on
|
|
249
|
+
both. The icon closure that receives a `ProductIconPhase` exists on
|
|
250
|
+
`ProductView(id:icon:placeholderIcon:)`, so an ID-based view with a phase
|
|
251
|
+
closure also needs `placeholderIcon`; `ProductView(_ product:icon:)` takes the
|
|
252
|
+
phase closure alone.
|
|
178
253
|
|
|
179
254
|
```swift
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
255
|
+
import StoreKit
|
|
256
|
+
import SwiftUI
|
|
257
|
+
|
|
258
|
+
struct AddOnShelf: View {
|
|
259
|
+
let ids: [String]
|
|
260
|
+
|
|
261
|
+
var body: some View {
|
|
262
|
+
StoreView(ids: ids, prefersPromotionalIcon: true)
|
|
263
|
+
.storeButton(.visible, for: .restorePurchases)
|
|
264
|
+
.productViewStyle(.large)
|
|
265
|
+
.onInAppPurchaseCompletion { _, outcome in await settle(outcome) }
|
|
188
266
|
}
|
|
189
|
-
|
|
267
|
+
}
|
|
190
268
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
269
|
+
struct FeaturedPack: View {
|
|
270
|
+
let id: String
|
|
271
|
+
|
|
272
|
+
var body: some View {
|
|
273
|
+
ProductView(id: id) { phase in
|
|
274
|
+
if case .success(let art) = phase {
|
|
275
|
+
art.resizable().aspectRatio(contentMode: .fit)
|
|
276
|
+
} else if case .loading = phase {
|
|
277
|
+
ProgressView()
|
|
278
|
+
} else {
|
|
279
|
+
Image(systemName: "shippingbox")
|
|
280
|
+
}
|
|
281
|
+
} placeholderIcon: {
|
|
282
|
+
ProgressView()
|
|
283
|
+
}
|
|
284
|
+
.productViewStyle(.large)
|
|
197
285
|
}
|
|
198
286
|
}
|
|
199
|
-
.productViewStyle(.large)
|
|
200
287
|
```
|
|
201
288
|
|
|
202
|
-
##
|
|
289
|
+
## Is a Plan Active?
|
|
203
290
|
|
|
204
291
|
```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
|
-
}
|
|
292
|
+
func hasActivePlan(in group: String) async throws -> Bool {
|
|
293
|
+
let statuses = try await Product.SubscriptionInfo.status(for: group)
|
|
294
|
+
return statuses.contains { entry in
|
|
295
|
+
guard case .verified = entry.transaction,
|
|
296
|
+
case .verified = entry.renewalInfo else { return false }
|
|
297
|
+
return [.subscribed, .inGracePeriod].contains(entry.state)
|
|
213
298
|
}
|
|
214
|
-
return false
|
|
215
299
|
}
|
|
216
300
|
```
|
|
217
301
|
|
|
218
|
-
|
|
302
|
+
A status counts only when both its renewal info and its transaction verify.
|
|
219
303
|
|
|
220
|
-
|
|
221
|
-
or detecting tampered installations (iOS 16+).
|
|
304
|
+
## Verifying the Install with AppTransaction
|
|
222
305
|
|
|
223
306
|
```swift
|
|
224
|
-
func
|
|
307
|
+
func checkInstall() async -> Date? {
|
|
225
308
|
do {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
let purchaseDate = appTransaction.originalPurchaseDate
|
|
231
|
-
// Migration logic for users who paid before subscription model
|
|
309
|
+
switch try await AppTransaction.shared {
|
|
310
|
+
case .verified(let install):
|
|
311
|
+
_ = install.originalAppVersion
|
|
312
|
+
return install.originalPurchaseDate
|
|
232
313
|
case .unverified:
|
|
233
|
-
|
|
234
|
-
break
|
|
314
|
+
return nil
|
|
235
315
|
}
|
|
236
|
-
} catch {
|
|
316
|
+
} catch {
|
|
317
|
+
return nil
|
|
318
|
+
}
|
|
237
319
|
}
|
|
238
320
|
```
|
|
239
321
|
|
|
240
|
-
|
|
322
|
+
With `.verified`, `originalAppVersion` and `originalPurchaseDate` tell you
|
|
323
|
+
whether this user bought the app before a switch to subscriptions, so you can
|
|
324
|
+
grandfather them. `.unverified` may mean a tampered install: restrict
|
|
325
|
+
features. The `catch` covers failing to fetch the app transaction at all.
|
|
326
|
+
|
|
327
|
+
## Store View Callbacks
|
|
241
328
|
|
|
242
329
|
```swift
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
330
|
+
struct TrackedStore: View {
|
|
331
|
+
let ids: [String]
|
|
332
|
+
let accountID: UUID
|
|
333
|
+
let log: @Sendable (String) -> Void
|
|
334
|
+
|
|
335
|
+
var body: some View {
|
|
336
|
+
StoreView(ids: ids)
|
|
337
|
+
.inAppPurchaseOptions { _ in [.appAccountToken(accountID)] }
|
|
338
|
+
.onInAppPurchaseStart { product in log("purchase_started \(product.id)") }
|
|
339
|
+
.onInAppPurchaseCompletion { _, outcome in await settle(outcome) }
|
|
250
340
|
}
|
|
251
341
|
}
|
|
252
|
-
.inAppPurchaseOptions { product in
|
|
253
|
-
[.appAccountToken(userAccountToken)]
|
|
254
|
-
}
|
|
255
342
|
```
|
|
256
343
|
|
|
257
|
-
|
|
344
|
+
`.onInAppPurchaseStart` suits analytics, `.onInAppPurchaseCompletion` does
|
|
345
|
+
verify, deliver, finish, and `.inAppPurchaseOptions` supplies options per
|
|
346
|
+
product.
|
|
258
347
|
|
|
259
|
-
|
|
348
|
+
## Ten Mistakes, Fixed
|
|
260
349
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
350
|
+
### 1. No listener at launch
|
|
351
|
+
|
|
352
|
+
Wrong: an `App` with an empty initializer never hears about renewals, refunds
|
|
353
|
+
or Ask to Buy approvals.
|
|
354
|
+
|
|
355
|
+
Right: start `Transaction.updates` in `init()`, as in
|
|
356
|
+
[Launch-Time Transaction Listener](#launch-time-transaction-listener).
|
|
357
|
+
|
|
358
|
+
### 2. Never finishing
|
|
359
|
+
|
|
360
|
+
Wrong: unlocking and returning. The transaction comes back in the unfinished
|
|
361
|
+
queue on every launch.
|
|
268
362
|
|
|
269
|
-
|
|
363
|
+
Right: record the delivery with a call that can fail, then finish. If the
|
|
364
|
+
record fails, leave the transaction unfinished so it is offered again.
|
|
270
365
|
|
|
271
366
|
```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()
|
|
367
|
+
func complete(_ receipt: StoreKit.Transaction, ledger: PurchaseLedger) async throws {
|
|
368
|
+
try await ledger.record(receipt.id, product: receipt.productID)
|
|
369
|
+
await receipt.finish()
|
|
370
|
+
}
|
|
280
371
|
```
|
|
281
372
|
|
|
282
|
-
### 3.
|
|
373
|
+
### 3. Trusting unverified data
|
|
283
374
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
let transaction = verification.unsafePayloadValue
|
|
375
|
+
Wrong: `check.unsafePayloadValue` hands you the payload whether or not the
|
|
376
|
+
signature holds; that is a security hole.
|
|
287
377
|
|
|
288
|
-
|
|
289
|
-
let transaction = try checkVerified(verification)
|
|
290
|
-
```
|
|
378
|
+
Right: `let receipt = try verified(check)`.
|
|
291
379
|
|
|
292
|
-
### 4.
|
|
380
|
+
### 4. Original IAP API in new code
|
|
293
381
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
let request = SKProductsRequest(productIdentifiers: ["com.app.premium"])
|
|
297
|
-
SKPaymentQueue.default().add(payment)
|
|
382
|
+
Avoid: `SKProductsRequest(productIdentifiers:)` and
|
|
383
|
+
`SKPaymentQueue.default().add(payment)`.
|
|
298
384
|
|
|
299
|
-
|
|
300
|
-
let products = try await Product.products(for: ["com.app.premium"])
|
|
301
|
-
let result = try await product.purchase()
|
|
302
|
-
```
|
|
385
|
+
Prefer: `Product.products(for:)` and `product.purchase()`.
|
|
303
386
|
|
|
304
|
-
### 5.
|
|
387
|
+
### 5. Ignoring revocation
|
|
305
388
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if case .verified(let transaction) = result {
|
|
309
|
-
purchased.insert(transaction.productID)
|
|
310
|
-
}
|
|
389
|
+
Wrong: inserting `productID` for every verified transaction also grants
|
|
390
|
+
refunded purchases.
|
|
311
391
|
|
|
312
|
-
|
|
313
|
-
if case .verified(let transaction) = result, transaction.revocationDate == nil {
|
|
314
|
-
purchased.insert(transaction.productID)
|
|
315
|
-
}
|
|
316
|
-
```
|
|
392
|
+
Right: require `revocationDate == nil` as well.
|
|
317
393
|
|
|
318
|
-
### 6.
|
|
394
|
+
### 6. Hard-coded prices
|
|
319
395
|
|
|
320
|
-
|
|
321
|
-
// WRONG: Wrong for other currencies and regions
|
|
322
|
-
Text("Buy Premium for $4.99")
|
|
396
|
+
Wrong: `Text("Pro for $4.99")` is wrong in every other currency and region.
|
|
323
397
|
|
|
324
|
-
|
|
325
|
-
Text("Buy \(product.displayName) for \(product.displayPrice)")
|
|
326
|
-
```
|
|
398
|
+
Right: `Text("\(product.displayName): \(product.displayPrice)")`.
|
|
327
399
|
|
|
328
|
-
### 7.
|
|
400
|
+
### 7. Dropping pending purchases
|
|
329
401
|
|
|
330
|
-
|
|
331
|
-
// WRONG: Silently drops pending Ask to Buy
|
|
332
|
-
default: break
|
|
402
|
+
Wrong: `default: break` in the result switch silently swallows Ask to Buy.
|
|
333
403
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
showPendingApprovalMessage()
|
|
337
|
-
```
|
|
404
|
+
Right: a `case .pending` that tells the user approval is pending; unlock
|
|
405
|
+
only when `Transaction.updates` delivers the approved transaction.
|
|
338
406
|
|
|
339
|
-
### 8.
|
|
407
|
+
### 8. One entitlement check at launch
|
|
340
408
|
|
|
341
|
-
|
|
342
|
-
// WRONG: Check once, never update
|
|
343
|
-
func appDidFinish() { Task { await updateEntitlements() } }
|
|
409
|
+
Wrong: computing entitlements once and caching them for the session.
|
|
344
410
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
```
|
|
411
|
+
Right: recompute from the `Transaction.updates` listener for changes during
|
|
412
|
+
the session, and again when the app returns to the foreground, for example
|
|
413
|
+
`.task { await store.refresh() }` on the content view.
|
|
349
414
|
|
|
350
|
-
### 9.
|
|
415
|
+
### 9. No restore control
|
|
351
416
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
SubscriptionStoreView(groupID: "group_id")
|
|
417
|
+
Wrong: a `SubscriptionStoreView` without restore, which App Review may
|
|
418
|
+
reject.
|
|
355
419
|
|
|
356
|
-
|
|
357
|
-
SubscriptionStoreView(groupID: "group_id")
|
|
358
|
-
.storeButton(.visible, for: .restorePurchases)
|
|
359
|
-
```
|
|
420
|
+
Right: `.storeButton(.visible, for: .restorePurchases)`.
|
|
360
421
|
|
|
361
|
-
### 10.
|
|
422
|
+
### 10. No policy links
|
|
362
423
|
|
|
363
|
-
|
|
364
|
-
// WRONG: No terms or privacy policy
|
|
365
|
-
SubscriptionStoreView(groupID: "group_id")
|
|
424
|
+
Wrong: a subscription screen with no Terms of Service or Privacy Policy.
|
|
366
425
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
.subscriptionStorePolicyDestination(url: termsURL, for: .termsOfService)
|
|
370
|
-
.subscriptionStorePolicyDestination(url: privacyURL, for: .privacyPolicy)
|
|
371
|
-
```
|
|
426
|
+
Right: both `.subscriptionStorePolicyDestination(url:for:)` calls, one for
|
|
427
|
+
`.termsOfService` and one for `.privacyPolicy`.
|