@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0

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