nccgs 1.0.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 (278) hide show
  1. package/.claude/agents/nccgs-accessibility-specialist.md +26 -0
  2. package/.claude/agents/nccgs-adversarial-reviewer.md +26 -0
  3. package/.claude/agents/nccgs-ai-programmer.md +26 -0
  4. package/.claude/agents/nccgs-analytics-engineer.md +26 -0
  5. package/.claude/agents/nccgs-art-direction-lead.md +26 -0
  6. package/.claude/agents/nccgs-audio-direction-lead.md +26 -0
  7. package/.claude/agents/nccgs-creative-director.md +26 -0
  8. package/.claude/agents/nccgs-documentation-manager.md +26 -0
  9. package/.claude/agents/nccgs-economy-designer.md +26 -0
  10. package/.claude/agents/nccgs-engine-programmer.md +26 -0
  11. package/.claude/agents/nccgs-game-design-lead.md +26 -0
  12. package/.claude/agents/nccgs-game-designer.md +26 -0
  13. package/.claude/agents/nccgs-gameplay-programmer.md +26 -0
  14. package/.claude/agents/nccgs-level-designer.md +26 -0
  15. package/.claude/agents/nccgs-live-ops-designer.md +26 -0
  16. package/.claude/agents/nccgs-localization-lead.md +26 -0
  17. package/.claude/agents/nccgs-narrative-lead.md +26 -0
  18. package/.claude/agents/nccgs-network-programmer.md +26 -0
  19. package/.claude/agents/nccgs-performance-analyst.md +26 -0
  20. package/.claude/agents/nccgs-producer.md +26 -0
  21. package/.claude/agents/nccgs-production-coordinator.md +23 -0
  22. package/.claude/agents/nccgs-programming-lead.md +26 -0
  23. package/.claude/agents/nccgs-prototyper.md +26 -0
  24. package/.claude/agents/nccgs-qa-engineer.md +26 -0
  25. package/.claude/agents/nccgs-qa-lead.md +26 -0
  26. package/.claude/agents/nccgs-release-engineer.md +26 -0
  27. package/.claude/agents/nccgs-release-lead.md +26 -0
  28. package/.claude/agents/nccgs-security-engineer.md +26 -0
  29. package/.claude/agents/nccgs-sound-designer.md +26 -0
  30. package/.claude/agents/nccgs-systems-designer.md +26 -0
  31. package/.claude/agents/nccgs-technical-architect.md +26 -0
  32. package/.claude/agents/nccgs-technical-artist.md +26 -0
  33. package/.claude/agents/nccgs-technical-director.md +26 -0
  34. package/.claude/agents/nccgs-tools-programmer.md +26 -0
  35. package/.claude/agents/nccgs-ui-programmer.md +26 -0
  36. package/.claude/agents/nccgs-unity-build-specialist.md +26 -0
  37. package/.claude/agents/nccgs-unity-content-specialist.md +26 -0
  38. package/.claude/agents/nccgs-unity-implementer.md +26 -0
  39. package/.claude/agents/nccgs-unity-rendering-specialist.md +26 -0
  40. package/.claude/agents/nccgs-unity-systems-specialist.md +26 -0
  41. package/.claude/agents/nccgs-unity-ui-specialist.md +26 -0
  42. package/.claude/agents/nccgs-ux-designer.md +26 -0
  43. package/.claude/agents/nccgs-verification-engineer.md +26 -0
  44. package/.claude/agents/nccgs-world-builder.md +26 -0
  45. package/.claude/agents/nccgs-writer.md +26 -0
  46. package/.claude/nccgs/THIRD_PARTY_NOTICES.md +13 -0
  47. package/.claude/nccgs/VERSION +1 -0
  48. package/.claude/nccgs/constitution.md +131 -0
  49. package/.claude/nccgs/hooks/agent-audit.mjs +10 -0
  50. package/.claude/nccgs/hooks/common.mjs +41 -0
  51. package/.claude/nccgs/hooks/post-compact.mjs +1 -0
  52. package/.claude/nccgs/hooks/pre-compact.mjs +8 -0
  53. package/.claude/nccgs/hooks/protect-git.mjs +22 -0
  54. package/.claude/nccgs/hooks/protect-write.mjs +17 -0
  55. package/.claude/nccgs/hooks/session-start.mjs +15 -0
  56. package/.claude/nccgs/hooks/session-stop.mjs +7 -0
  57. package/.claude/nccgs/protocols/agent-contract.md +34 -0
  58. package/.claude/nccgs/protocols/context-packets.md +13 -0
  59. package/.claude/nccgs/protocols/evidence.md +15 -0
  60. package/.claude/nccgs/protocols/model-routing.md +22 -0
  61. package/.claude/nccgs/protocols/orchestration.md +26 -0
  62. package/.claude/nccgs/protocols/unity-boundary.md +11 -0
  63. package/.claude/nccgs/settings.fragment.json +77 -0
  64. package/.claude/nccgs/studio.json +347 -0
  65. package/.claude/nccgs/tools/configure-models.mjs +39 -0
  66. package/.claude/nccgs/unity-skills-manifest.json +727 -0
  67. package/.claude/nccgs/workflow-catalog.json +317 -0
  68. package/.claude/rules/nccgs-canonical-docs.md +19 -0
  69. package/.claude/rules/nccgs-editor-tools.md +12 -0
  70. package/.claude/rules/nccgs-localization.md +12 -0
  71. package/.claude/rules/nccgs-networking.md +13 -0
  72. package/.claude/rules/nccgs-performance.md +14 -0
  73. package/.claude/rules/nccgs-rendering.md +16 -0
  74. package/.claude/rules/nccgs-security.md +13 -0
  75. package/.claude/rules/nccgs-tests.md +18 -0
  76. package/.claude/rules/nccgs-ui.md +14 -0
  77. package/.claude/rules/nccgs-unity-assets.md +21 -0
  78. package/.claude/rules/nccgs-unity-code.md +22 -0
  79. package/.claude/skills/accessibility-review/SKILL.md +14 -0
  80. package/.claude/skills/architecture-decision/SKILL.md +14 -0
  81. package/.claude/skills/asset-audit/SKILL.md +14 -0
  82. package/.claude/skills/audit/SKILL.md +16 -0
  83. package/.claude/skills/audit/references/dimensions.md +46 -0
  84. package/.claude/skills/balance-review/SKILL.md +14 -0
  85. package/.claude/skills/bug-triage/SKILL.md +14 -0
  86. package/.claude/skills/build-live-game/SKILL.md +317 -0
  87. package/.claude/skills/build-live-game/references/achievements.md +779 -0
  88. package/.claude/skills/build-live-game/references/apis.md +280 -0
  89. package/.claude/skills/build-live-game/references/authentication.md +437 -0
  90. package/.claude/skills/build-live-game/references/battlepass.md +860 -0
  91. package/.claude/skills/build-live-game/references/cloud-code.md +563 -0
  92. package/.claude/skills/build-live-game/references/cloud-save.md +474 -0
  93. package/.claude/skills/build-live-game/references/deployment.md +216 -0
  94. package/.claude/skills/build-live-game/references/player-account.md +813 -0
  95. package/.claude/skills/build-live-game/references/remote-config.md +96 -0
  96. package/.claude/skills/build-live-game/references/tooling.md +431 -0
  97. package/.claude/skills/closure/SKILL.md +14 -0
  98. package/.claude/skills/code-review/SKILL.md +14 -0
  99. package/.claude/skills/compatibility-review/SKILL.md +14 -0
  100. package/.claude/skills/context-pack/SKILL.md +14 -0
  101. package/.claude/skills/dependency-review/SKILL.md +14 -0
  102. package/.claude/skills/design/SKILL.md +20 -0
  103. package/.claude/skills/design-review/SKILL.md +14 -0
  104. package/.claude/skills/evidence-review/SKILL.md +14 -0
  105. package/.claude/skills/hotfix/SKILL.md +14 -0
  106. package/.claude/skills/implement-in-app-purchases/README.md +233 -0
  107. package/.claude/skills/implement-in-app-purchases/SKILL.md +158 -0
  108. package/.claude/skills/implement-in-app-purchases/references/api-notes.md +562 -0
  109. package/.claude/skills/implement-in-app-purchases/references/codeless-catalog.md +331 -0
  110. package/.claude/skills/implement-in-app-purchases/references/convert-adapty.md +268 -0
  111. package/.claude/skills/implement-in-app-purchases/references/convert-essentialkit.md +239 -0
  112. package/.claude/skills/implement-in-app-purchases/references/convert-revenuecat.md +275 -0
  113. package/.claude/skills/implement-in-app-purchases/references/convert-unipay.md +145 -0
  114. package/.claude/skills/implement-in-app-purchases/references/migration-v4-to-v5.md +204 -0
  115. package/.claude/skills/implement-in-app-purchases/references/path-add-iap-to-new-project.md +198 -0
  116. package/.claude/skills/implement-in-app-purchases/references/path-convert-native-google-billing.md +356 -0
  117. package/.claude/skills/implement-in-app-purchases/references/path-convert-native-storekit.md +446 -0
  118. package/.claude/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md +680 -0
  119. package/.claude/skills/implement-in-app-purchases/references/platform-notes.md +204 -0
  120. package/.claude/skills/implement-in-app-purchases/references/pre-check.md +205 -0
  121. package/.claude/skills/incident-recovery/SKILL.md +14 -0
  122. package/.claude/skills/initialize-ai-navigation/SKILL.md +95 -0
  123. package/.claude/skills/initialize-ai-navigation/references/navigation-system.md +794 -0
  124. package/.claude/skills/levelplay-unity-integration/CHANGELOG.md +57 -0
  125. package/.claude/skills/levelplay-unity-integration/README.md +74 -0
  126. package/.claude/skills/levelplay-unity-integration/SKILL.md +1126 -0
  127. package/.claude/skills/levelplay-unity-integration/references/banner-api.md +920 -0
  128. package/.claude/skills/levelplay-unity-integration/references/best-practices.md +536 -0
  129. package/.claude/skills/levelplay-unity-integration/references/ilrd-api.md +337 -0
  130. package/.claude/skills/levelplay-unity-integration/references/initialization-api.md +630 -0
  131. package/.claude/skills/levelplay-unity-integration/references/interstitial-api.md +899 -0
  132. package/.claude/skills/levelplay-unity-integration/references/ios-setup.md +491 -0
  133. package/.claude/skills/levelplay-unity-integration/references/migration-sdk-9.md +666 -0
  134. package/.claude/skills/levelplay-unity-integration/references/privacy-settings.md +608 -0
  135. package/.claude/skills/levelplay-unity-integration/references/rewarded-api.md +902 -0
  136. package/.claude/skills/localization/SKILL.md +135 -0
  137. package/.claude/skills/localization/references/api-notes.md +76 -0
  138. package/.claude/skills/localization/resources/L10nBatchProcessor.cs +69 -0
  139. package/.claude/skills/localization/resources/LocalizedFontAsset.cs +18 -0
  140. package/.claude/skills/localize-game/SKILL.md +14 -0
  141. package/.claude/skills/migrate-project/SKILL.md +21 -0
  142. package/.claude/skills/migrate-project/references/procedure.md +63 -0
  143. package/.claude/skills/milestone-review/SKILL.md +14 -0
  144. package/.claude/skills/new-unity-project/SKILL.md +179 -0
  145. package/.claude/skills/optimize-audio/SKILL.md +199 -0
  146. package/.claude/skills/optimize-audio/resources/audio-import-api.md +146 -0
  147. package/.claude/skills/optimize-audio/resources/platform-settings.md +48 -0
  148. package/.claude/skills/optimize-text-mesh-pro/SKILL.md +182 -0
  149. package/.claude/skills/optimize-web/SKILL.md +393 -0
  150. package/.claude/skills/optimize-web/resources/WebOptimizer.cs +21 -0
  151. package/.claude/skills/optimize-web/resources/toktx-examples.sh +11 -0
  152. package/.claude/skills/performance-audit/SKILL.md +14 -0
  153. package/.claude/skills/physics-3d-collision/SKILL.md +442 -0
  154. package/.claude/skills/physics-3d-collision/references/troubleshooting.md +41 -0
  155. package/.claude/skills/physics-3d-collision/resources/CollisionDebugger.cs +33 -0
  156. package/.claude/skills/plan-feature/SKILL.md +14 -0
  157. package/.claude/skills/playtest/SKILL.md +14 -0
  158. package/.claude/skills/project-stage/SKILL.md +14 -0
  159. package/.claude/skills/prototype-feature/SKILL.md +14 -0
  160. package/.claude/skills/qa-plan/SKILL.md +14 -0
  161. package/.claude/skills/release/SKILL.md +18 -0
  162. package/.claude/skills/release-readiness/SKILL.md +14 -0
  163. package/.claude/skills/retrospective/SKILL.md +14 -0
  164. package/.claude/skills/review/SKILL.md +16 -0
  165. package/.claude/skills/security-audit/SKILL.md +14 -0
  166. package/.claude/skills/setup-multiplayer-services/SKILL.md +39 -0
  167. package/.claude/skills/setup-multiplayer-services/references/dgs-entrypoint.md +79 -0
  168. package/.claude/skills/setup-multiplayer-services/references/entrypoints.md +213 -0
  169. package/.claude/skills/setup-multiplayer-services/references/examples.md +33 -0
  170. package/.claude/skills/setup-multiplayer-services/references/implementation-fit.md +30 -0
  171. package/.claude/skills/setup-multiplayer-services/references/underlying-services.md +11 -0
  172. package/.claude/skills/setup-multiplayer-services/references/workflows-prerequisites.md +17 -0
  173. package/.claude/skills/setup-vivox-voice-chat/SKILL.md +118 -0
  174. package/.claude/skills/setup-vivox-voice-chat/evals/.env.example +6 -0
  175. package/.claude/skills/setup-vivox-voice-chat/evals/README.md +101 -0
  176. package/.claude/skills/setup-vivox-voice-chat/evals/promptfooconfig.yaml +32 -0
  177. package/.claude/skills/setup-vivox-voice-chat/evals/tests/init-and-login.yaml +76 -0
  178. package/.claude/skills/setup-vivox-voice-chat/evals/tests/text-chat.yaml +65 -0
  179. package/.claude/skills/setup-vivox-voice-chat/evals/tests/voice-channels.yaml +74 -0
  180. package/.claude/skills/setup-vivox-voice-chat/references/events-and-participants.md +79 -0
  181. package/.claude/skills/setup-vivox-voice-chat/references/init-and-login.md +92 -0
  182. package/.claude/skills/setup-vivox-voice-chat/references/text-chat.md +93 -0
  183. package/.claude/skills/setup-vivox-voice-chat/references/troubleshooting.md +47 -0
  184. package/.claude/skills/setup-vivox-voice-chat/references/voice-channels.md +90 -0
  185. package/.claude/skills/shader-graph-create-custom-node/SKILL.md +25 -0
  186. package/.claude/skills/shader-graph-create-custom-node/resources/all_hints.hlsl +182 -0
  187. package/.claude/skills/sprint-plan/SKILL.md +14 -0
  188. package/.claude/skills/sprite-editor/SKILL.md +66 -0
  189. package/.claude/skills/sprite-editor/references/api_reference.md +151 -0
  190. package/.claude/skills/sprite-editor/references/background.md +112 -0
  191. package/.claude/skills/sprite-editor/references/templates.md +72 -0
  192. package/.claude/skills/sprite-editor/scripts/AutomaticSliceTexture.cs +40 -0
  193. package/.claude/skills/sprite-editor/scripts/GenerateNewSpriteRects.cs +200 -0
  194. package/.claude/skills/sprite-editor/scripts/GetTextureSourceImageSize.cs +32 -0
  195. package/.claude/skills/sprite-editor/scripts/GetTextureToSlice.cs +55 -0
  196. package/.claude/skills/sprite-editor/scripts/GridSliceTexture.cs +40 -0
  197. package/.claude/skills/sprite-editor/scripts/IsometricSliceTexture.cs +141 -0
  198. package/.claude/skills/sprite-editor/scripts/README.md +134 -0
  199. package/.claude/skills/sprite-editor/scripts/SetPivotExample.cs +58 -0
  200. package/.claude/skills/sprite-editor/scripts/SpriteToPng.cs +88 -0
  201. package/.claude/skills/status/SKILL.md +16 -0
  202. package/.claude/skills/story-readiness/SKILL.md +14 -0
  203. package/.claude/skills/test/SKILL.md +16 -0
  204. package/.claude/skills/ui/SKILL.md +142 -0
  205. package/.claude/skills/ui-imgui/SKILL.md +186 -0
  206. package/.claude/skills/ui-imgui/references/gui-elements.md +156 -0
  207. package/.claude/skills/ui-imgui/references/templates.md +141 -0
  208. package/.claude/skills/ui-review/SKILL.md +14 -0
  209. package/.claude/skills/ui-ugui/SKILL.md +282 -0
  210. package/.claude/skills/ui-ugui/references/scrollview-setup.md +29 -0
  211. package/.claude/skills/ui-uitk/SKILL.md +235 -0
  212. package/.claude/skills/ui-uitk/references/common-issues.md +74 -0
  213. package/.claude/skills/ui-uitk/references/custom-elements.md +241 -0
  214. package/.claude/skills/ui-uitk/references/painter2d.md +282 -0
  215. package/.claude/skills/ui-uitk/references/pointermanipulator-guide.md +94 -0
  216. package/.claude/skills/ui-uitk/references/svg-icons.md +136 -0
  217. package/.claude/skills/ui-uitk/references/ui-runtime-binding.md +234 -0
  218. package/.claude/skills/ui-uitk/references/uss-guide.md +138 -0
  219. package/.claude/skills/unity-cli/CHANGELOG.md +233 -0
  220. package/.claude/skills/unity-cli/SECURITY.md +22 -0
  221. package/.claude/skills/unity-cli/SKILL.md +414 -0
  222. package/.claude/skills/unity-cli/references/auth-license-cloud.md +146 -0
  223. package/.claude/skills/unity-cli/references/build-run-test.md +349 -0
  224. package/.claude/skills/unity-cli/references/collaboration.md +472 -0
  225. package/.claude/skills/unity-cli/references/config-hub.md +103 -0
  226. package/.claude/skills/unity-cli/references/diagnostics-maintenance.md +326 -0
  227. package/.claude/skills/unity-cli/references/editors-install.md +327 -0
  228. package/.claude/skills/unity-cli/references/integration-advanced.md +472 -0
  229. package/.claude/skills/unity-cli/references/projects-templates.md +574 -0
  230. package/.claude/skills/unity-package-management/SKILL.md +304 -0
  231. package/.claude/skills/unity-package-management/references/select-packages.md +108 -0
  232. package/.claude/skills/urp-postprocessing/SKILL.md +188 -0
  233. package/.claude/skills/urp-postprocessing/references/code-templates.md +119 -0
  234. package/.claude/skills/urp-postprocessing/references/effect-reference.md +86 -0
  235. package/.claude/skills/validate-urp-render-graph-renderer-feature/SKILL.md +269 -0
  236. package/.claude/skills/work/SKILL.md +36 -0
  237. package/.claude/skills/work/references/classification.md +41 -0
  238. package/.claude/skills/work/references/closure.md +50 -0
  239. package/.claude/skills/work/references/feature-contracts.md +32 -0
  240. package/.claude/skills/work/references/verification.md +26 -0
  241. package/CLAUDE.md +6 -0
  242. package/LICENSE +21 -0
  243. package/README.md +162 -0
  244. package/THIRD_PARTY_NOTICES.md +23 -0
  245. package/UPGRADING.md +32 -0
  246. package/VERSION +1 -0
  247. package/docs/ARCHITECTURE.md +73 -0
  248. package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +324 -0
  249. package/docs/MIGRATION-MATRIX.md +23 -0
  250. package/docs/PROJECT-POLICY.md +67 -0
  251. package/docs/WORKFLOWS.md +61 -0
  252. package/package-assets/setup-vivox-voice-chat-evals.gitignore +7 -0
  253. package/package.json +41 -0
  254. package/scaffold/.nccgs/bugs/.gitkeep +1 -0
  255. package/scaffold/.nccgs/closures/.gitkeep +1 -0
  256. package/scaffold/.nccgs/context/.gitkeep +1 -0
  257. package/scaffold/.nccgs/decisions/.gitkeep +1 -0
  258. package/scaffold/.nccgs/evidence/.gitkeep +1 -0
  259. package/scaffold/.nccgs/features/.gitkeep +1 -0
  260. package/scaffold/.nccgs/migrations/.gitkeep +1 -0
  261. package/scaffold/.nccgs/playtests/.gitkeep +1 -0
  262. package/scaffold/.nccgs/project.yaml +103 -0
  263. package/scaffold/.nccgs/requirements.yaml +10 -0
  264. package/scaffold/.nccgs/reviews/.gitkeep +1 -0
  265. package/scaffold/.nccgs/state.md +40 -0
  266. package/scaffold/.nccgs/templates/agent-handoff.md +25 -0
  267. package/scaffold/.nccgs/templates/architecture-decision.md +27 -0
  268. package/scaffold/.nccgs/templates/closure-record.md +51 -0
  269. package/scaffold/.nccgs/templates/context-packet.yaml +17 -0
  270. package/scaffold/.nccgs/templates/evidence-record.md +23 -0
  271. package/scaffold/.nccgs/templates/feature-contract.md +35 -0
  272. package/scaffold/.nccgs/templates/migration-plan.md +40 -0
  273. package/scaffold/.nccgs/templates/waiver.md +11 -0
  274. package/scripts/cli.mjs +56 -0
  275. package/scripts/install.mjs +267 -0
  276. package/scripts/sync-unity-skills.mjs +126 -0
  277. package/scripts/validate.mjs +205 -0
  278. package/tests/framework.test.mjs +121 -0
@@ -0,0 +1,204 @@
1
+ # Unity IAP v4 to v5 Migration Guide
2
+
3
+ ## Table of Contents
4
+
5
+ - [Trigger Phrases](#trigger-phrases)
6
+ - [Overview](#overview)
7
+ - [Migration Mapping Table](#migration-mapping-table)
8
+ - [Key Breaking Changes](#key-breaking-changes)
9
+ - [Migration Anti-Patterns](#migration-anti-patterns)
10
+ - [Minimal v5 Example](#minimal-v5-example)
11
+
12
+ ## Trigger Phrases
13
+
14
+ - "Migrate from Unity IAP v4 to v5"
15
+ - "Upgrade Unity IAP to v5"
16
+ - "Update IAP from v4"
17
+ - "Migrate IStoreListener to StoreController"
18
+ - "Replace ConfigurationBuilder with v5 IAP"
19
+ - "Update UnityPurchasing.Initialize to v5"
20
+
21
+ ## Overview
22
+
23
+ Unity IAP v5 replaces the listener-based pattern (`IStoreListener`) with an event-driven pattern (`StoreController` events). This is a breaking change that requires updating all IAP code.
24
+
25
+ ## Migration Mapping Table
26
+
27
+ ### Initialization
28
+
29
+ | v4 (Legacy) | v5 (Current) |
30
+ |---|---|
31
+ | `UnityPurchasing.Initialize(listener, builder)` | `UnityIAPServices.StoreController()` then `await store.Connect()` |
32
+ | `ConfigurationBuilder.Instance(StandardPurchasingModule.Instance())` | Use `CatalogProvider` for store-specific IDs/payouts, or pass `List<ProductDefinition>` directly to `FetchProducts()` |
33
+ | `builder.AddProduct("id", ProductType.Consumable)` | `new ProductDefinition("id", ProductType.Consumable)` in a list |
34
+ | `IStoreListener.OnInitialized(controller, extensions)` | `store.OnStoreConnected` event |
35
+ | `IStoreListener.OnInitializeFailed(error)` | `store.OnStoreDisconnected` event |
36
+
37
+ ### Purchase Flow
38
+
39
+ | v4 (Legacy) | v5 (Current) |
40
+ |---|---|
41
+ | `controller.InitiatePurchase(product)` | `store.PurchaseProduct(product)` — note: `Purchase()` no longer accepts a developer payload argument (removed in Google Billing v3) |
42
+ | `IStoreListener.ProcessPurchase(args)` for **new** purchases | `store.OnPurchasePending` + `store.ConfirmPurchase(pendingOrder)` |
43
+ | `IStoreListener.ProcessPurchase(args)` for **restored** purchases | `store.OnPurchasesFetched` — use existing `ProcessPurchase` logic in both `OnPurchasePending` and `OnPurchasesFetched` |
44
+ | `IStoreListener.ProcessPurchase(args)` returning `PurchaseProcessingResult.Pending` | Just don't call `ConfirmPurchase` until ready — store the `PendingOrder` reference |
45
+ | `IStoreListener.OnPurchaseFailed(product, reason)` | `store.OnPurchaseFailed` event with `FailedOrder` (has `FailureReason` and `Details`) |
46
+ | `controller.ConfirmPendingPurchase(product)` | `store.ConfirmPurchase(pendingOrder)` — requires the `PendingOrder` object, not the `Product` |
47
+ | `product.hasReceipt` | **Preferred:** call `store.CheckEntitlement(product)` and handle `store.OnCheckEntitlement` — check `entitlement.Status == EntitlementStatus.FullyEntitled`. **Alternative:** track ownership with a `bool` flag updated in `OnPurchasePending` (new) and `OnPurchasesFetched` (restored). `CheckEntitlement` is the recommended v5 pattern for non-consumables and subscriptions. |
48
+
49
+ ### Configuration / Products
50
+
51
+ | v4 (Legacy) | v5 (Current) |
52
+ |---|---|
53
+ | `ConfigurationBuilder` with `AddProduct()` | `CatalogProvider` with `AddProduct()`, or a `List<ProductDefinition>` passed to `store.FetchProducts()` |
54
+ | `builder.AddProduct("id", type, new IDs { { "store_id", store } })` | `catalogProvider.AddProduct("id", type, new StoreSpecificIds { { "store_id", store } })` — or `new ProductDefinition("id", "store_id", type)` for a single store ID |
55
+ | `controller.products.WithID("id")` | `store.GetProductById("id")` |
56
+ | `controller.products.all` | `store.GetProducts()` |
57
+
58
+ ### Restore Transactions
59
+
60
+ | v4 (Legacy) | v5 (Current) |
61
+ |---|---|
62
+ | `extensions.GetExtension<IAppleExtensions>().RestoreTransactions(callback)` | `store.RestoreTransactions(callback)` |
63
+ | Manual restore call required | Confirmed purchases are **automatically restored** when you call `store.FetchPurchases()` or `store.CheckEntitlement()` — `RestoreTransactions` is only needed for explicit user-triggered restore buttons (required on iOS) |
64
+
65
+ ### Receipt Validation
66
+
67
+ | v4 (Legacy) | v5 (Current) |
68
+ |---|---|
69
+ | `CrossPlatformValidator` with Apple + Google | `CrossPlatformValidator` still works, but under StoreKit 2 Apple validation returns an empty array (no-op). **Recommended:** Use the Google-only constructor: `CrossPlatformValidator(GooglePlayTangle.Data(), Application.identifier)`. The legacy 4-arg constructor still works. Single-bundle-ID constructors are `[Obsolete]` |
70
+ | `AppleTangle.Data()` for Apple validation | Under StoreKit 2, local Apple receipt validation is a no-op. Use `order.Info.Apple?.jwsRepresentation` for server-side Apple validation instead |
71
+ | Manual receipt parsing | Use `order.Info.Apple?.jwsRepresentation` for server-side validation |
72
+ | `product.receipt` | `order.Info.Receipt` — receipt is now accessed from the `Order`/`PendingOrder`, not the `Product` |
73
+
74
+ ### Platform Extensions
75
+
76
+ | v4 (Legacy) | v5 (Current) |
77
+ |---|---|
78
+ | `extensions.GetExtension<IAppleExtensions>()` | `store.AppleStoreExtendedService` / `store.AppleStoreExtendedPurchaseService` |
79
+ | `extensions.GetExtension<IGooglePlayStoreExtensions>()` | `store.GooglePlayStoreExtendedService` / `store.GooglePlayStoreExtendedPurchaseService` |
80
+ | `builder.Configure<IAppleConfiguration>().SetApplePromotionalPurchaseInterceptorCallback(cb)` | `store.AppleStoreExtendedPurchaseService.OnPromotionalPurchaseIntercepted += cb` (event on `IAppleStoreExtendedPurchaseService`, null-check required; callback signature: `Action<Product>`) |
81
+ | `appleExtensions.ContinuePromotionalPurchases()` | `store.AppleStoreExtendedPurchaseService?.ContinuePromotionalPurchases()` |
82
+ | `appleExtensions.RegisterPurchaseDeferredListener(cb)` | `store.OnPurchaseDeferred += cb` (event on `StoreController`) |
83
+ | `appleExtensions.simulateAskToBuy` | `store.AppleStoreExtendedPurchaseService?.simulateAskToBuy` (null-check required) |
84
+ | `appleExtensions.PresentCodeRedemptionSheet()` | `store.AppleStoreExtendedPurchaseService?.PresentCodeRedemptionSheet()` |
85
+ | `appleExtensions.RestoreTransactions(cb)` | `store.RestoreTransactions(cb)` |
86
+ | `appleExtensions.SetApplicationUsername(hashedString)` | `store.AppleStoreExtendedService?.SetAppAccountToken(Guid)` — must be called **after** `Connect()`; accepts a `Guid` (not a hash) identifying the user account in your system |
87
+ | `builder.Configure<IAppleConfiguration>().SetEntitlementsRevokedListener(cb)` | `store.AppleStoreExtendedPurchaseService.OnEntitlementRevoked += cb` (event on `IAppleStoreExtendedPurchaseService`, null-check required; callback signature: `Action<string>` — receives a single product ID, NOT `List<Product>`) |
88
+ | `appleExtensions.GetTransactionReceiptForProduct(product)` | `order.Info.Receipt` inside `OnPurchasePending` — per-transaction receipt is now on the order |
89
+ | `builder.Configure<IAppleConfiguration>().canMakePayments` | `store.AppleStoreExtendedService?.canMakePayments` — must be checked **after** `Connect()`, not before initialization |
90
+ | `appleExtensions.GetIntroductoryPriceDictionary()` | `store.AppleStoreExtendedProductService?.GetIntroductoryPriceDictionary()` — returns `Dictionary<string, string>` mapping product store-specific IDs to JSON with intro offer details. **NOT removed**, just moved to the product extension service. `AppleProductMetadata` does NOT expose `introductoryPrice`, `introductoryPriceLocale`, `introductoryNumberOfPeriods`, or `subscriptionPeriod` — those fields do not exist on that class. For introductory price data, use `SubscriptionInfo` methods (`GetIntroductoryPrice()`, `GetIntroductoryPricePeriod()`, `GetIntroductoryPricePeriodCycles()`). |
91
+ | `appleExtensions.GetProductDetails()` | `store.AppleStoreExtendedProductService?.GetProductDetails()` — returns `Dictionary<string, string>`. **NOT removed**, just moved to the product extension service. Basic metadata (title, description, price) is on `product.metadata` directly. |
92
+ | `appleExtensions.SetStorePromotionOrder(products)` | `store.AppleStoreExtendedProductService?.SetStorePromotionOrder(products)` — moved to `IAppleStoreExtendedProductService` (null-check required) |
93
+ | `appleExtensions.SetStorePromotionVisibility(product, visibility)` | `store.AppleStoreExtendedProductService?.SetStorePromotionVisibility(product, visibility)` — moved to `IAppleStoreExtendedProductService` (null-check required) |
94
+ | `appleExtensions.FetchStorePromotionOrder(success, failure)` | `store.AppleStoreExtendedProductService?.FetchStorePromotionOrder(successCb, errorCb)` — moved to `IAppleStoreExtendedProductService` (null-check required; callback signatures: `Action<List<Product>>`, `Action<string>`) |
95
+ | `appleExtensions.FetchStorePromotionVisibility(product, success, failure)` | `store.AppleStoreExtendedProductService?.FetchStorePromotionVisibility(product, successCb, errorCb)` — moved to `IAppleStoreExtendedProductService` (null-check required; callback signatures: `Action<string, AppleStorePromotionVisibility>`, `Action<string>`) |
96
+ | `googlePlayConfig.SetDeferredPurchaseListener(cb)` | `store.OnPurchaseDeferred += cb` (event on `StoreController`) |
97
+ | `googlePlayConfig.SetDeferredProrationUpgradeDowngradeSubscriptionListener(cb)` | `store.GooglePlayStoreExtendedPurchaseService.OnDeferredPaymentUntilRenewalDate += cb` (event on `IGooglePlayStoreExtendedPurchaseService`, null-check required; callback signature: `Action<DeferredPaymentUntilRenewalDateOrder>` — NOT `Action<Product>`. Access the product via `deferredOrder.SubscriptionOrdered`) |
98
+ | `googlePlayExtensions.UpgradeDowngradeSubscription(currentId, newId, mode)` | `store.GooglePlayStoreExtendedPurchaseService?.UpgradeDowngradeSubscription(currentOrder, newProduct, desiredReplacementMode)` — takes `Order` and `Product` objects instead of string IDs; third parameter is `GooglePlayReplacementMode` (not the deprecated `GooglePlayProrationMode`); call after `Connect()` |
99
+ | `googlePlayExtensions.IsPurchasedProductDeferred(product)` | `store.GooglePlayStoreExtendedPurchaseService?.IsOrderDeferred(order)` — renamed and takes `Order` instead of `Product`; marked `[Obsolete]`. Prefer tracking deferred state via `store.OnPurchaseDeferred` / `store.OnPurchasePending` events instead |
100
+ | `googlePlayExtensions.RestoreTransactions(cb)` | `store.RestoreTransactions(cb)` — moved to `StoreController` directly |
101
+ | `googlePlayConfig.SetObfuscatedAccountId(id)` | `store.GooglePlayStoreExtendedService?.SetObfuscatedAccountId(id)` — moved from config-time to **post-`Connect()`** |
102
+ | `googlePlayConfig.SetObfuscatedProfileId(id)` | `store.GooglePlayStoreExtendedService?.SetObfuscatedProfileId(id)` — moved from config-time to **post-`Connect()`** |
103
+
104
+ ### Subscription Info
105
+
106
+ | v4 (Legacy) | v5 (Current) |
107
+ |---|---|
108
+ | `new SubscriptionManager(product, introJson).getSubscriptionInfo()` | `order.Info.PurchasedProductInfo.FirstOrDefault(p => p.productId == productId)?.subscriptionInfo` — accessed via `IPurchasedProductInfo` on the order's info, NOT on `CartItem`. `CartItem` only has `Product` and `Quantity`. |
109
+ | `subscriptionInfo.isSubscribed() == Result.True` | `purchasedProductInfo?.subscriptionInfo?.IsSubscribed() == Result.True` — method is now PascalCase but still returns `Result` enum (True/False/Unsupported), NOT `bool`. Use `== Result.True` for null-safe comparison (returns `false` if the chain is null). Do NOT use `?? false` — `Result?` and `bool` are incompatible types. |
110
+ | `product.receipt == null` to check ownership | **Preferred:** use `store.CheckEntitlement(product)` + `store.OnCheckEntitlement` — the recommended v5 ownership check. **Alternative:** track ownership with a `bool` flag updated in `OnPurchasePending` (new purchase) and `OnPurchasesFetched` (restored purchases). |
111
+ | `product.metadata.GetAppleProductMetadata()?.isFamilyShareable` | Still valid — `GetAppleProductMetadata()` extension method on `ProductMetadata` is unchanged |
112
+
113
+ ### Codeless IAP
114
+
115
+ | v4 (Legacy) | v5 (Current) |
116
+ |---|---|
117
+ | `CodelessIAPStoreListener.Instance` | Codeless still works but uses v5 under the hood |
118
+ | `CodelessIAPStoreListener.initializationComplete` | `CodelessIAPStoreListener.IsInitialized()` |
119
+
120
+ ## Key Breaking Changes
121
+
122
+ 1. **`IStoreListener` is deprecated**: Replace the interface implementation with event subscriptions on `StoreController`. The interface still compiles but is `[Obsolete]`.
123
+ 2. **Two-step purchase flow is mandatory**: v5 always uses pending → confirm. There is no equivalent of returning `PurchaseProcessingResult.Complete` from `ProcessPurchase`.
124
+ 3. **`ConfigurationBuilder` is deprecated**: Use `CatalogProvider` for store-specific IDs and payouts, or pass `List<ProductDefinition>` directly to `FetchProducts()`. The class still compiles but is `[Obsolete]`.
125
+ 4. **Apple local receipt validation is a no-op under StoreKit 2**: `CrossPlatformValidator` itself is not deprecated, but Apple validation silently returns an empty array under StoreKit 2. **Recommended:** use the Google-only constructor `CrossPlatformValidator(GooglePlayTangle.Data(), Application.identifier)`. Single-bundle-ID constructors are `[Obsolete]`. For server-side Apple validation, use `order.Info.Apple?.jwsRepresentation`.
126
+ 5. **Extensions are direct properties**: No more `GetExtension<T>()` pattern. Access via `store.AppleStoreExtendedService`, `store.AppleStoreExtendedPurchaseService`, `store.GooglePlayStoreExtendedService`, `store.GooglePlayStoreExtendedPurchaseService` (null-check required — only non-null on the matching platform).
127
+ 6. **`product.receipt` and `product.hasReceipt` are gone**: Use `order.Info.Receipt` from `PendingOrder` for the transaction receipt. For ownership checking, use `store.CheckEntitlement(product)` + `OnCheckEntitlement` (recommended) or track ownership with `bool` flags updated in `OnPurchasePending` and `OnPurchasesFetched`.
128
+ 7. **`Purchase()` no longer has a payload**: Developer payload support was removed in Google Billing v3.
129
+ 8. **`ProcessPurchase` logic belongs in two places**: Put it in both `OnPurchasePending` (new purchases) and `OnPurchasesFetched` (restored/existing purchases).
130
+ 9. **Restore is implicit**: Calling `FetchPurchases()` automatically re-delivers any unconfirmed purchases via `OnPurchasePending`. Explicit `RestoreTransactions()` is still required for the iOS "Restore Purchases" button.
131
+ 10. **`SubscriptionManager` is replaced**: No more `new SubscriptionManager(product, introJson).getSubscriptionInfo()`. In v5, subscription info is accessed via `order.Info.PurchasedProductInfo.FirstOrDefault(p => p.productId == id)?.subscriptionInfo` — it's on `IPurchasedProductInfo`, NOT on `CartItem`. `CartItem` only has `Product` and `Quantity`. Method names are now PascalCase: `IsSubscribed()` not `isSubscribed()`.
132
+ 11. **Platform-specific config must happen post-`Connect()`**: `SetAppAccountToken`, `SetObfuscatedAccountId/ProfileId`, `canMakePayments` — all moved from pre-init `ConfigurationBuilder` to the corresponding extended service, only available after `await store.Connect()`.
133
+ 12. **Apple promotional APIs moved to `IAppleStoreExtendedProductService`**: `SetStorePromotionOrder`, `SetStorePromotionVisibility`, `FetchStorePromotionOrder`, `FetchStorePromotionVisibility` are now on `store.AppleStoreExtendedProductService` (null-check required — only non-null on Apple platforms). No more `GetExtension<IAppleExtensions>()` pattern.
134
+ 13. **`OnPurchaseConfirmed` receives `Order` base type**: You MUST pattern-match `ConfirmedOrder` (success) vs `FailedOrder` (confirmation failed). Do not assume confirmation always succeeds.
135
+ 14. **Platform-specific events are on extended services, NOT `StoreController`**: `OnPromotionalPurchaseIntercepted` and `OnEntitlementRevoked` are on `AppleStoreExtendedPurchaseService`. `OnDeferredPaymentUntilRenewalDate` is on `GooglePlayStoreExtendedPurchaseService`. Only `OnPurchaseDeferred` (Ask-to-Buy) is directly on `StoreController`. Always null-check the extended service before subscribing to events (use `if (store.AppleStoreExtendedPurchaseService != null)` pattern — `?.` does not work with `+=`).
136
+
137
+ ## Migration Anti-Patterns
138
+
139
+ **Always subscribe to BOTH success and failure events.** Not subscribing to failure events (e.g., `OnProductsFetchFailed`, `OnPurchasesFetchFailed`, `OnStoreDisconnected`) generates runtime warnings and leaves failures unhandled.
140
+
141
+ **Always subscribe to `OnPurchaseDeferred`.** This event fires for Ask-to-Buy (iOS parental approval) and Google Play deferred purchases. Not subscribing means deferred purchases are silently ignored.
142
+
143
+ **Use `CheckEntitlement` for ownership checks, not manual bool flags.** When migrating `product.hasReceipt` or `product.receipt == null` ownership checks, the recommended v5 pattern is `store.CheckEntitlement(product)` + `store.OnCheckEntitlement`. This works cross-platform and handles edge cases (refunds, subscription expiration) automatically.
144
+
145
+ **`StoreConnectionFailureDescription` has `.message`, NOT `.reason`.** When migrating `OnInitializeFailed(error, message)` to `store.OnStoreDisconnected`, the failure description property is `failureDescription.message` (lowercase), not `reason`.
146
+
147
+ **`ProductFetchFailed` has `.FailureReason`, NOT `.Message`.** When handling `store.OnProductsFetchFailed`, access the reason via `failure.FailureReason`.
148
+
149
+ **Subscribe to events BEFORE calling `Connect()`.** Pending purchases from a previous session may fire immediately on reconnect. If your `OnPurchasePending` handler is not yet registered, those purchases will be missed.
150
+
151
+ **`OnPurchaseConfirmed` can receive `FailedOrder`.** The `OnPurchaseConfirmed` event fires with `Order` (base type). Always pattern-match: `ConfirmedOrder` means success, `FailedOrder` means confirmation failed. Do not assume confirmation always succeeds.
152
+
153
+ **Use `Awake()` for initialization, not `Start()`.** The official v5 samples use `Awake()` for `StoreController` setup and event subscription. This ensures IAP is initialized before other `Start()` methods that may depend on it.
154
+
155
+ ## Minimal v5 Example
156
+
157
+ ```csharp
158
+ StoreController m_StoreController;
159
+
160
+ async void Awake()
161
+ {
162
+ m_StoreController = UnityIAPServices.StoreController();
163
+
164
+ // Subscribe to ALL events BEFORE Connect — pending purchases may fire on reconnect
165
+ m_StoreController.OnPurchasePending += (order) =>
166
+ {
167
+ var product = order.CartOrdered.Items().FirstOrDefault()?.Product;
168
+ GrantContent(product);
169
+ m_StoreController.ConfirmPurchase(order);
170
+ };
171
+ m_StoreController.OnPurchaseConfirmed += (order) =>
172
+ {
173
+ switch (order)
174
+ {
175
+ case ConfirmedOrder: Debug.Log("Purchase confirmed"); break;
176
+ case FailedOrder failed: Debug.LogError($"Confirmation failed: {failed.FailureReason}"); break;
177
+ }
178
+ };
179
+ m_StoreController.OnPurchaseFailed += (failed) => Debug.LogError($"{failed.FailureReason} - {failed.Details}");
180
+ m_StoreController.OnPurchaseDeferred += (deferred) => Debug.Log("Purchase deferred (e.g., Ask-to-Buy)");
181
+
182
+ m_StoreController.OnStoreConnected += OnStoreConnected;
183
+ m_StoreController.OnStoreDisconnected += (failure) => Debug.LogError($"Store disconnected: {failure.Message}");
184
+
185
+ await m_StoreController.Connect();
186
+ }
187
+
188
+ void OnStoreConnected()
189
+ {
190
+ var products = new List<ProductDefinition>
191
+ {
192
+ new ProductDefinition("com.mygame.coins100", ProductType.Consumable)
193
+ };
194
+
195
+ m_StoreController.OnProductsFetched += (fetched) => Debug.Log("Products ready");
196
+ m_StoreController.OnProductsFetchFailed += (failure) => Debug.LogError($"Product fetch failed: {failure.FailureReason}");
197
+ m_StoreController.FetchProducts(products);
198
+
199
+ // Restore any pending/unfinished purchases from the platform store
200
+ m_StoreController.OnPurchasesFetched += (orders) => Debug.Log($"Restored {orders.PendingOrders.Count} pending purchases");
201
+ m_StoreController.OnPurchasesFetchFailed += (failure) => Debug.LogError($"Purchase fetch failed: {failure.Message}");
202
+ m_StoreController.FetchPurchases();
203
+ }
204
+ ```
@@ -0,0 +1,198 @@
1
+ # Add Unity IAP 5 to a Project With No IAP
2
+
3
+ ## Table of Contents
4
+
5
+ - [Step 1 — Package Installation](#step-1--package-installation)
6
+ - [Step 2 — Project Scan](#step-2--project-scan)
7
+ - [Step 3 — Product Discovery](#step-3--product-discovery)
8
+ - [Step 4 — IAPManager Architecture](#step-4--iapmanager-architecture)
9
+ - [Step 5 — Purchase Handling Contract](#step-5--purchase-handling-contract)
10
+ - [Step 6 — Product Type Behavior Rules](#step-6--product-type-behavior-rules)
11
+ - [Step 7 — Cloud Save Integration](#step-7--cloud-save-integration)
12
+ - [Step 8 — UI Integration](#step-8--ui-integration)
13
+ - [Step 9 — Verification Report](#step-9--verification-report)
14
+
15
+ Use this reference when the project has no existing in-app purchase implementation and needs Unity IAP 5 added from scratch.
16
+
17
+ ## Step 1 — Package Installation
18
+
19
+ Check `Packages/manifest.json` for `com.unity.purchasing`:
20
+
21
+ - **If absent:** Install via **Window > Package Manager > Unity Registry > In App Purchasing**. Do not edit `manifest.json` directly unless the user explicitly allows it. Confirm installation before proceeding.
22
+ - **If present but below v5.0:** Instruct the user to upgrade via Package Manager to the latest stable v5 release. Do not proceed until the upgrade is confirmed.
23
+ - **If v5.0+ is already present:** Note the exact version and proceed.
24
+
25
+ Always use the latest stable v5 release unless the user specifies a version. Never downgrade an existing package.
26
+
27
+ ## Step 2 — Project Scan
28
+
29
+ ### 2a — Code scan
30
+
31
+ Search `Assets/**/*.cs` for any existing IAP signals:
32
+
33
+ ```
34
+ ProductType|ProductDefinition|StoreController|IAPButton|CodelessIAP
35
+ ProcessPurchase|PendingOrder|DeferredOrder|ConfirmPurchase|FetchPurchases
36
+ IStoreListener|UnityPurchasing\.Initialize|ConfigurationBuilder
37
+ ```
38
+
39
+ ### 2b — Inventory and economy scan
40
+
41
+ Search `Assets/**/*.cs` for terms that indicate what needs to be credited after purchase:
42
+
43
+ ```
44
+ \bcoins\b|\bgems\b|\blives\b|\binventory\b|\bcurrency\b
45
+ PlayerData|SaveAsync|CloudSave|SaveDataAsync|SaveGame
46
+ Economy
47
+ ```
48
+
49
+ ### 2c — Shop UI scan
50
+
51
+ Search `Assets/**/*.unity`, `*.prefab`, `*.asset` for shop-related GameObjects and scripts:
52
+ ```
53
+ Shop|Store|Purchase|Buy|IAP|Product|Monetiz
54
+ ```
55
+
56
+ Collect: scene names, prefab paths, button component names, and any serialized product ID strings.
57
+
58
+ ## Step 3 — Product Discovery
59
+
60
+ ### If product definitions are already found (from Step 2a/2b/2c)
61
+
62
+ Use them. Confirm types with the user if ambiguous.
63
+
64
+ ### If no product definitions exist
65
+
66
+ **Stop and ask:**
67
+
68
+ > "Please provide the first IAP product ID and type — for example `com.mygame.coins100` as Consumable — and tell me which inventory field, currency, or item should be credited after purchase."
69
+
70
+ - If the user provides only a product ID with no type, **default to Consumable** but state the assumption explicitly before proceeding.
71
+ - Collect all products before writing any code. For each product record: `productId`, `ProductType`, reward target (field name / method name / amount).
72
+
73
+ ## Step 4 — IAPManager Architecture
74
+
75
+ ### Match the project's existing patterns
76
+
77
+ Before generating code:
78
+ - Check whether the project uses MonoBehaviour singletons, ScriptableObject services, or dependency injection.
79
+ - Check the namespace convention used in `Assets/Scripts/`.
80
+ - Prefer the pattern already in use rather than introducing a new one.
81
+
82
+ ### IAPManager responsibilities
83
+
84
+ Create a single `IAPManager` (MonoBehaviour singleton or service, matching project pattern) that:
85
+
86
+ - Holds the `StoreController` instance.
87
+ - Exposes `Buy(string productId)`.
88
+ - Exposes `RestorePurchases()` — **only** if the product list contains `NonConsumable` or `Subscription` types.
89
+ - Fires UI-friendly events or callbacks for: `OnInitialized`, `OnProductsLoaded`, `OnPurchaseSuccess`, `OnPurchaseFailed`, `OnPurchaseDeferred`.
90
+ - Initializes once in `Awake()` — see the **Initialization Flow** section in SKILL.md for the exact event subscription and `Connect()` ordering rules.
91
+ - Registers all products from a single authoritative product list (not scattered across UI handlers).
92
+
93
+ ### Product catalog
94
+
95
+ Define products in one place — a `ScriptableObject`, a plain `List<ProductDefinition>`, or a constants class — not inside button click handlers. Wire button click handlers to `IAPManager.Buy(productId)` using the catalog, not hardcoded strings.
96
+
97
+ ## Step 5 — Purchase Handling Contract
98
+
99
+ For API mechanics (two-step flow, event names, `ConfirmPurchase` signature) see the **Two-Step Purchase Flow** and **Required Event Subscriptions** sections in SKILL.md. This section covers only the grant-and-save contract that is specific to this path.
100
+
101
+ ### PendingOrder — the save-before-confirm rule
102
+
103
+ ```
104
+ OnPurchasePending fires
105
+ → grant reward to inventory / currency / entitlement
106
+ → save player data (see Cloud Save Integration below)
107
+ → ONLY IF save succeeds: call ConfirmPurchase(pendingOrder)
108
+ → IF save fails: do NOT confirm — store will re-deliver on next launch
109
+ ```
110
+
111
+ Never call `ConfirmPurchase` before the save completes. An unconfirmed purchase is safe — it re-delivers. A confirmed purchase that was never saved is a lost reward.
112
+
113
+ ### Duplicate grant prevention
114
+
115
+ `OnPurchasePending` may fire more than once for the same purchase (app restart before confirmation). Track processed order IDs (e.g., in Cloud Save or a local ledger) and skip grant if the order ID was already processed.
116
+
117
+ ### DeferredOrder
118
+
119
+ Do not grant anything. Fire `OnPurchaseDeferred` event to update UI ("Purchase pending approval"). Wait for `OnPurchasePending` when the purchase is approved.
120
+
121
+ ### FailedOrder
122
+
123
+ Do not grant anything. Fire `OnPurchaseFailed` with the reason. See **Failure Description Property Names** in SKILL.md for the correct property names per type.
124
+
125
+ ## Step 6 — Product Type Behavior Rules
126
+
127
+ ### Consumable
128
+
129
+ - Credit inventory or currency after purchase.
130
+ - Persist the credited state in save data before confirming.
131
+ - **Do not restore** old consumable orders — confirmed consumables are not returned by `FetchPurchases` and must not be re-granted.
132
+ - Track consumable grants yourself (order ID ledger in Cloud Save or local save).
133
+
134
+ ### NonConsumable
135
+
136
+ - Unlock a durable entitlement (feature flag, item ownership).
137
+ - Include `RestorePurchases()` — required on iOS, good practice on Android.
138
+ - Re-apply the entitlement on app startup: call `store.FetchPurchases()` and re-check ownership in `OnPurchasesFetched`, or use `store.CheckEntitlement(product)`. See **Entitlement Checking** and **Fetch Existing Purchases** in SKILL.md.
139
+
140
+ ### Subscription
141
+
142
+ - Restore or check subscription state on app startup, not only at purchase time. Subscriptions can expire or be cancelled externally.
143
+ - Use `store.CheckEntitlement(product)` or inspect `OnPurchasesFetched` results to update active/expired/unknown state.
144
+ - See **Subscription Info** in SKILL.md for the correct access path (`order.Info.PurchasedProductInfo`, `IsSubscribed() == Result.True`).
145
+
146
+ ## Step 7 — Cloud Save Integration
147
+
148
+ ### Detect the existing save system first
149
+
150
+ Search for any of these patterns before writing save code:
151
+
152
+ ```
153
+ \.SaveAsync\(\)|CloudSaveService\.Instance
154
+ SaveGame\(|SaveDataAsync\(|PlayerPrefs\.SetInt|JsonUtility\.ToJson|JsonConvert\.Serialize
155
+ ```
156
+
157
+ ### Rules
158
+
159
+ - **Use the existing save abstraction** — do not create a new save system.
160
+ - Prefer methods already in use: `CloudSaveService.Instance.Data.Player.SaveAsync()`, `SaveGame()`, `SaveDataAsync()`, or equivalent.
161
+ - If no save system exists, use `PlayerPrefs` as a minimal fallback and document it as a TODO for the developer to upgrade.
162
+ - Save **must complete before `ConfirmPurchase`** is called (see Step 5).
163
+
164
+ ## Step 8 — UI Integration
165
+
166
+ ### Wiring
167
+
168
+ - Wire existing shop buttons to `IAPManager.Buy(productId)` — do not embed product IDs in button click handlers directly.
169
+ - Subscribe to `IAPManager` events in the shop UI script to drive state changes.
170
+
171
+ ### States to handle in UI
172
+
173
+ | State | Trigger | UI action |
174
+ |---|---|---|
175
+ | Initializing | Before `OnInitialized` | Disable buy buttons or show spinner |
176
+ | Products loaded | `OnProductsLoaded` | Display localized price from `product.metadata.localizedPriceString` |
177
+ | Product unavailable | Product missing from `OnProductsFetched` result | Hide or grey out the button |
178
+ | Purchase pending | `PurchaseProduct()` called | Disable button, show loading state |
179
+ | Purchase deferred | `OnPurchaseDeferred` | Show "pending approval" message |
180
+ | Purchase success | `OnPurchaseSuccess` | Show confirmation, update inventory display |
181
+ | Purchase failed | `OnPurchaseFailed` | Show error message, re-enable button |
182
+
183
+ ### Restore button
184
+
185
+ Add a "Restore Purchases" button **only** if the product list contains `NonConsumable` or `Subscription` products. Required on iOS. Wire to `IAPManager.RestorePurchases()`.
186
+
187
+ ## Step 9 — Verification Report
188
+
189
+ After applying changes, produce a report with these sections:
190
+
191
+ 1. **Files changed** — list with nature of each change
192
+ 2. **Product IDs and types** — final catalog
193
+ 3. **Reward mapping** — product ID → field/method credited
194
+ 4. **Save behavior** — which save method is called, when
195
+ 5. **Restore behavior** — which products are restorable, how
196
+ 6. **Pending / deferred handling** — confirmation of save-before-confirm and deferred UI
197
+ 7. **Duplicate grant prevention** — how order IDs are tracked
198
+ 8. **Manual steps still required** — Unity Editor steps (Receipt Validation Obfuscator if using local Google Play validation), App Store Connect / Play Console product setup, sandbox testing accounts