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.
- package/.claude/agents/nccgs-accessibility-specialist.md +26 -0
- package/.claude/agents/nccgs-adversarial-reviewer.md +26 -0
- package/.claude/agents/nccgs-ai-programmer.md +26 -0
- package/.claude/agents/nccgs-analytics-engineer.md +26 -0
- package/.claude/agents/nccgs-art-direction-lead.md +26 -0
- package/.claude/agents/nccgs-audio-direction-lead.md +26 -0
- package/.claude/agents/nccgs-creative-director.md +26 -0
- package/.claude/agents/nccgs-documentation-manager.md +26 -0
- package/.claude/agents/nccgs-economy-designer.md +26 -0
- package/.claude/agents/nccgs-engine-programmer.md +26 -0
- package/.claude/agents/nccgs-game-design-lead.md +26 -0
- package/.claude/agents/nccgs-game-designer.md +26 -0
- package/.claude/agents/nccgs-gameplay-programmer.md +26 -0
- package/.claude/agents/nccgs-level-designer.md +26 -0
- package/.claude/agents/nccgs-live-ops-designer.md +26 -0
- package/.claude/agents/nccgs-localization-lead.md +26 -0
- package/.claude/agents/nccgs-narrative-lead.md +26 -0
- package/.claude/agents/nccgs-network-programmer.md +26 -0
- package/.claude/agents/nccgs-performance-analyst.md +26 -0
- package/.claude/agents/nccgs-producer.md +26 -0
- package/.claude/agents/nccgs-production-coordinator.md +23 -0
- package/.claude/agents/nccgs-programming-lead.md +26 -0
- package/.claude/agents/nccgs-prototyper.md +26 -0
- package/.claude/agents/nccgs-qa-engineer.md +26 -0
- package/.claude/agents/nccgs-qa-lead.md +26 -0
- package/.claude/agents/nccgs-release-engineer.md +26 -0
- package/.claude/agents/nccgs-release-lead.md +26 -0
- package/.claude/agents/nccgs-security-engineer.md +26 -0
- package/.claude/agents/nccgs-sound-designer.md +26 -0
- package/.claude/agents/nccgs-systems-designer.md +26 -0
- package/.claude/agents/nccgs-technical-architect.md +26 -0
- package/.claude/agents/nccgs-technical-artist.md +26 -0
- package/.claude/agents/nccgs-technical-director.md +26 -0
- package/.claude/agents/nccgs-tools-programmer.md +26 -0
- package/.claude/agents/nccgs-ui-programmer.md +26 -0
- package/.claude/agents/nccgs-unity-build-specialist.md +26 -0
- package/.claude/agents/nccgs-unity-content-specialist.md +26 -0
- package/.claude/agents/nccgs-unity-implementer.md +26 -0
- package/.claude/agents/nccgs-unity-rendering-specialist.md +26 -0
- package/.claude/agents/nccgs-unity-systems-specialist.md +26 -0
- package/.claude/agents/nccgs-unity-ui-specialist.md +26 -0
- package/.claude/agents/nccgs-ux-designer.md +26 -0
- package/.claude/agents/nccgs-verification-engineer.md +26 -0
- package/.claude/agents/nccgs-world-builder.md +26 -0
- package/.claude/agents/nccgs-writer.md +26 -0
- package/.claude/nccgs/THIRD_PARTY_NOTICES.md +13 -0
- package/.claude/nccgs/VERSION +1 -0
- package/.claude/nccgs/constitution.md +131 -0
- package/.claude/nccgs/hooks/agent-audit.mjs +10 -0
- package/.claude/nccgs/hooks/common.mjs +41 -0
- package/.claude/nccgs/hooks/post-compact.mjs +1 -0
- package/.claude/nccgs/hooks/pre-compact.mjs +8 -0
- package/.claude/nccgs/hooks/protect-git.mjs +22 -0
- package/.claude/nccgs/hooks/protect-write.mjs +17 -0
- package/.claude/nccgs/hooks/session-start.mjs +15 -0
- package/.claude/nccgs/hooks/session-stop.mjs +7 -0
- package/.claude/nccgs/protocols/agent-contract.md +34 -0
- package/.claude/nccgs/protocols/context-packets.md +13 -0
- package/.claude/nccgs/protocols/evidence.md +15 -0
- package/.claude/nccgs/protocols/model-routing.md +22 -0
- package/.claude/nccgs/protocols/orchestration.md +26 -0
- package/.claude/nccgs/protocols/unity-boundary.md +11 -0
- package/.claude/nccgs/settings.fragment.json +77 -0
- package/.claude/nccgs/studio.json +347 -0
- package/.claude/nccgs/tools/configure-models.mjs +39 -0
- package/.claude/nccgs/unity-skills-manifest.json +727 -0
- package/.claude/nccgs/workflow-catalog.json +317 -0
- package/.claude/rules/nccgs-canonical-docs.md +19 -0
- package/.claude/rules/nccgs-editor-tools.md +12 -0
- package/.claude/rules/nccgs-localization.md +12 -0
- package/.claude/rules/nccgs-networking.md +13 -0
- package/.claude/rules/nccgs-performance.md +14 -0
- package/.claude/rules/nccgs-rendering.md +16 -0
- package/.claude/rules/nccgs-security.md +13 -0
- package/.claude/rules/nccgs-tests.md +18 -0
- package/.claude/rules/nccgs-ui.md +14 -0
- package/.claude/rules/nccgs-unity-assets.md +21 -0
- package/.claude/rules/nccgs-unity-code.md +22 -0
- package/.claude/skills/accessibility-review/SKILL.md +14 -0
- package/.claude/skills/architecture-decision/SKILL.md +14 -0
- package/.claude/skills/asset-audit/SKILL.md +14 -0
- package/.claude/skills/audit/SKILL.md +16 -0
- package/.claude/skills/audit/references/dimensions.md +46 -0
- package/.claude/skills/balance-review/SKILL.md +14 -0
- package/.claude/skills/bug-triage/SKILL.md +14 -0
- package/.claude/skills/build-live-game/SKILL.md +317 -0
- package/.claude/skills/build-live-game/references/achievements.md +779 -0
- package/.claude/skills/build-live-game/references/apis.md +280 -0
- package/.claude/skills/build-live-game/references/authentication.md +437 -0
- package/.claude/skills/build-live-game/references/battlepass.md +860 -0
- package/.claude/skills/build-live-game/references/cloud-code.md +563 -0
- package/.claude/skills/build-live-game/references/cloud-save.md +474 -0
- package/.claude/skills/build-live-game/references/deployment.md +216 -0
- package/.claude/skills/build-live-game/references/player-account.md +813 -0
- package/.claude/skills/build-live-game/references/remote-config.md +96 -0
- package/.claude/skills/build-live-game/references/tooling.md +431 -0
- package/.claude/skills/closure/SKILL.md +14 -0
- package/.claude/skills/code-review/SKILL.md +14 -0
- package/.claude/skills/compatibility-review/SKILL.md +14 -0
- package/.claude/skills/context-pack/SKILL.md +14 -0
- package/.claude/skills/dependency-review/SKILL.md +14 -0
- package/.claude/skills/design/SKILL.md +20 -0
- package/.claude/skills/design-review/SKILL.md +14 -0
- package/.claude/skills/evidence-review/SKILL.md +14 -0
- package/.claude/skills/hotfix/SKILL.md +14 -0
- package/.claude/skills/implement-in-app-purchases/README.md +233 -0
- package/.claude/skills/implement-in-app-purchases/SKILL.md +158 -0
- package/.claude/skills/implement-in-app-purchases/references/api-notes.md +562 -0
- package/.claude/skills/implement-in-app-purchases/references/codeless-catalog.md +331 -0
- package/.claude/skills/implement-in-app-purchases/references/convert-adapty.md +268 -0
- package/.claude/skills/implement-in-app-purchases/references/convert-essentialkit.md +239 -0
- package/.claude/skills/implement-in-app-purchases/references/convert-revenuecat.md +275 -0
- package/.claude/skills/implement-in-app-purchases/references/convert-unipay.md +145 -0
- package/.claude/skills/implement-in-app-purchases/references/migration-v4-to-v5.md +204 -0
- package/.claude/skills/implement-in-app-purchases/references/path-add-iap-to-new-project.md +198 -0
- package/.claude/skills/implement-in-app-purchases/references/path-convert-native-google-billing.md +356 -0
- package/.claude/skills/implement-in-app-purchases/references/path-convert-native-storekit.md +446 -0
- package/.claude/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md +680 -0
- package/.claude/skills/implement-in-app-purchases/references/platform-notes.md +204 -0
- package/.claude/skills/implement-in-app-purchases/references/pre-check.md +205 -0
- package/.claude/skills/incident-recovery/SKILL.md +14 -0
- package/.claude/skills/initialize-ai-navigation/SKILL.md +95 -0
- package/.claude/skills/initialize-ai-navigation/references/navigation-system.md +794 -0
- package/.claude/skills/levelplay-unity-integration/CHANGELOG.md +57 -0
- package/.claude/skills/levelplay-unity-integration/README.md +74 -0
- package/.claude/skills/levelplay-unity-integration/SKILL.md +1126 -0
- package/.claude/skills/levelplay-unity-integration/references/banner-api.md +920 -0
- package/.claude/skills/levelplay-unity-integration/references/best-practices.md +536 -0
- package/.claude/skills/levelplay-unity-integration/references/ilrd-api.md +337 -0
- package/.claude/skills/levelplay-unity-integration/references/initialization-api.md +630 -0
- package/.claude/skills/levelplay-unity-integration/references/interstitial-api.md +899 -0
- package/.claude/skills/levelplay-unity-integration/references/ios-setup.md +491 -0
- package/.claude/skills/levelplay-unity-integration/references/migration-sdk-9.md +666 -0
- package/.claude/skills/levelplay-unity-integration/references/privacy-settings.md +608 -0
- package/.claude/skills/levelplay-unity-integration/references/rewarded-api.md +902 -0
- package/.claude/skills/localization/SKILL.md +135 -0
- package/.claude/skills/localization/references/api-notes.md +76 -0
- package/.claude/skills/localization/resources/L10nBatchProcessor.cs +69 -0
- package/.claude/skills/localization/resources/LocalizedFontAsset.cs +18 -0
- package/.claude/skills/localize-game/SKILL.md +14 -0
- package/.claude/skills/migrate-project/SKILL.md +21 -0
- package/.claude/skills/migrate-project/references/procedure.md +63 -0
- package/.claude/skills/milestone-review/SKILL.md +14 -0
- package/.claude/skills/new-unity-project/SKILL.md +179 -0
- package/.claude/skills/optimize-audio/SKILL.md +199 -0
- package/.claude/skills/optimize-audio/resources/audio-import-api.md +146 -0
- package/.claude/skills/optimize-audio/resources/platform-settings.md +48 -0
- package/.claude/skills/optimize-text-mesh-pro/SKILL.md +182 -0
- package/.claude/skills/optimize-web/SKILL.md +393 -0
- package/.claude/skills/optimize-web/resources/WebOptimizer.cs +21 -0
- package/.claude/skills/optimize-web/resources/toktx-examples.sh +11 -0
- package/.claude/skills/performance-audit/SKILL.md +14 -0
- package/.claude/skills/physics-3d-collision/SKILL.md +442 -0
- package/.claude/skills/physics-3d-collision/references/troubleshooting.md +41 -0
- package/.claude/skills/physics-3d-collision/resources/CollisionDebugger.cs +33 -0
- package/.claude/skills/plan-feature/SKILL.md +14 -0
- package/.claude/skills/playtest/SKILL.md +14 -0
- package/.claude/skills/project-stage/SKILL.md +14 -0
- package/.claude/skills/prototype-feature/SKILL.md +14 -0
- package/.claude/skills/qa-plan/SKILL.md +14 -0
- package/.claude/skills/release/SKILL.md +18 -0
- package/.claude/skills/release-readiness/SKILL.md +14 -0
- package/.claude/skills/retrospective/SKILL.md +14 -0
- package/.claude/skills/review/SKILL.md +16 -0
- package/.claude/skills/security-audit/SKILL.md +14 -0
- package/.claude/skills/setup-multiplayer-services/SKILL.md +39 -0
- package/.claude/skills/setup-multiplayer-services/references/dgs-entrypoint.md +79 -0
- package/.claude/skills/setup-multiplayer-services/references/entrypoints.md +213 -0
- package/.claude/skills/setup-multiplayer-services/references/examples.md +33 -0
- package/.claude/skills/setup-multiplayer-services/references/implementation-fit.md +30 -0
- package/.claude/skills/setup-multiplayer-services/references/underlying-services.md +11 -0
- package/.claude/skills/setup-multiplayer-services/references/workflows-prerequisites.md +17 -0
- package/.claude/skills/setup-vivox-voice-chat/SKILL.md +118 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/.env.example +6 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/README.md +101 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/promptfooconfig.yaml +32 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/tests/init-and-login.yaml +76 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/tests/text-chat.yaml +65 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/tests/voice-channels.yaml +74 -0
- package/.claude/skills/setup-vivox-voice-chat/references/events-and-participants.md +79 -0
- package/.claude/skills/setup-vivox-voice-chat/references/init-and-login.md +92 -0
- package/.claude/skills/setup-vivox-voice-chat/references/text-chat.md +93 -0
- package/.claude/skills/setup-vivox-voice-chat/references/troubleshooting.md +47 -0
- package/.claude/skills/setup-vivox-voice-chat/references/voice-channels.md +90 -0
- package/.claude/skills/shader-graph-create-custom-node/SKILL.md +25 -0
- package/.claude/skills/shader-graph-create-custom-node/resources/all_hints.hlsl +182 -0
- package/.claude/skills/sprint-plan/SKILL.md +14 -0
- package/.claude/skills/sprite-editor/SKILL.md +66 -0
- package/.claude/skills/sprite-editor/references/api_reference.md +151 -0
- package/.claude/skills/sprite-editor/references/background.md +112 -0
- package/.claude/skills/sprite-editor/references/templates.md +72 -0
- package/.claude/skills/sprite-editor/scripts/AutomaticSliceTexture.cs +40 -0
- package/.claude/skills/sprite-editor/scripts/GenerateNewSpriteRects.cs +200 -0
- package/.claude/skills/sprite-editor/scripts/GetTextureSourceImageSize.cs +32 -0
- package/.claude/skills/sprite-editor/scripts/GetTextureToSlice.cs +55 -0
- package/.claude/skills/sprite-editor/scripts/GridSliceTexture.cs +40 -0
- package/.claude/skills/sprite-editor/scripts/IsometricSliceTexture.cs +141 -0
- package/.claude/skills/sprite-editor/scripts/README.md +134 -0
- package/.claude/skills/sprite-editor/scripts/SetPivotExample.cs +58 -0
- package/.claude/skills/sprite-editor/scripts/SpriteToPng.cs +88 -0
- package/.claude/skills/status/SKILL.md +16 -0
- package/.claude/skills/story-readiness/SKILL.md +14 -0
- package/.claude/skills/test/SKILL.md +16 -0
- package/.claude/skills/ui/SKILL.md +142 -0
- package/.claude/skills/ui-imgui/SKILL.md +186 -0
- package/.claude/skills/ui-imgui/references/gui-elements.md +156 -0
- package/.claude/skills/ui-imgui/references/templates.md +141 -0
- package/.claude/skills/ui-review/SKILL.md +14 -0
- package/.claude/skills/ui-ugui/SKILL.md +282 -0
- package/.claude/skills/ui-ugui/references/scrollview-setup.md +29 -0
- package/.claude/skills/ui-uitk/SKILL.md +235 -0
- package/.claude/skills/ui-uitk/references/common-issues.md +74 -0
- package/.claude/skills/ui-uitk/references/custom-elements.md +241 -0
- package/.claude/skills/ui-uitk/references/painter2d.md +282 -0
- package/.claude/skills/ui-uitk/references/pointermanipulator-guide.md +94 -0
- package/.claude/skills/ui-uitk/references/svg-icons.md +136 -0
- package/.claude/skills/ui-uitk/references/ui-runtime-binding.md +234 -0
- package/.claude/skills/ui-uitk/references/uss-guide.md +138 -0
- package/.claude/skills/unity-cli/CHANGELOG.md +233 -0
- package/.claude/skills/unity-cli/SECURITY.md +22 -0
- package/.claude/skills/unity-cli/SKILL.md +414 -0
- package/.claude/skills/unity-cli/references/auth-license-cloud.md +146 -0
- package/.claude/skills/unity-cli/references/build-run-test.md +349 -0
- package/.claude/skills/unity-cli/references/collaboration.md +472 -0
- package/.claude/skills/unity-cli/references/config-hub.md +103 -0
- package/.claude/skills/unity-cli/references/diagnostics-maintenance.md +326 -0
- package/.claude/skills/unity-cli/references/editors-install.md +327 -0
- package/.claude/skills/unity-cli/references/integration-advanced.md +472 -0
- package/.claude/skills/unity-cli/references/projects-templates.md +574 -0
- package/.claude/skills/unity-package-management/SKILL.md +304 -0
- package/.claude/skills/unity-package-management/references/select-packages.md +108 -0
- package/.claude/skills/urp-postprocessing/SKILL.md +188 -0
- package/.claude/skills/urp-postprocessing/references/code-templates.md +119 -0
- package/.claude/skills/urp-postprocessing/references/effect-reference.md +86 -0
- package/.claude/skills/validate-urp-render-graph-renderer-feature/SKILL.md +269 -0
- package/.claude/skills/work/SKILL.md +36 -0
- package/.claude/skills/work/references/classification.md +41 -0
- package/.claude/skills/work/references/closure.md +50 -0
- package/.claude/skills/work/references/feature-contracts.md +32 -0
- package/.claude/skills/work/references/verification.md +26 -0
- package/CLAUDE.md +6 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/THIRD_PARTY_NOTICES.md +23 -0
- package/UPGRADING.md +32 -0
- package/VERSION +1 -0
- package/docs/ARCHITECTURE.md +73 -0
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +324 -0
- package/docs/MIGRATION-MATRIX.md +23 -0
- package/docs/PROJECT-POLICY.md +67 -0
- package/docs/WORKFLOWS.md +61 -0
- package/package-assets/setup-vivox-voice-chat-evals.gitignore +7 -0
- package/package.json +41 -0
- package/scaffold/.nccgs/bugs/.gitkeep +1 -0
- package/scaffold/.nccgs/closures/.gitkeep +1 -0
- package/scaffold/.nccgs/context/.gitkeep +1 -0
- package/scaffold/.nccgs/decisions/.gitkeep +1 -0
- package/scaffold/.nccgs/evidence/.gitkeep +1 -0
- package/scaffold/.nccgs/features/.gitkeep +1 -0
- package/scaffold/.nccgs/migrations/.gitkeep +1 -0
- package/scaffold/.nccgs/playtests/.gitkeep +1 -0
- package/scaffold/.nccgs/project.yaml +103 -0
- package/scaffold/.nccgs/requirements.yaml +10 -0
- package/scaffold/.nccgs/reviews/.gitkeep +1 -0
- package/scaffold/.nccgs/state.md +40 -0
- package/scaffold/.nccgs/templates/agent-handoff.md +25 -0
- package/scaffold/.nccgs/templates/architecture-decision.md +27 -0
- package/scaffold/.nccgs/templates/closure-record.md +51 -0
- package/scaffold/.nccgs/templates/context-packet.yaml +17 -0
- package/scaffold/.nccgs/templates/evidence-record.md +23 -0
- package/scaffold/.nccgs/templates/feature-contract.md +35 -0
- package/scaffold/.nccgs/templates/migration-plan.md +40 -0
- package/scaffold/.nccgs/templates/waiver.md +11 -0
- package/scripts/cli.mjs +56 -0
- package/scripts/install.mjs +267 -0
- package/scripts/sync-unity-skills.mjs +126 -0
- package/scripts/validate.mjs +205 -0
- package/tests/framework.test.mjs +121 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
# Unity IAP API Notes
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Anti-Hallucination: v5 vs Legacy API](#anti-hallucination-v5-vs-legacy-api)
|
|
6
|
+
- [StoreController (v5) Key Members](#storecontroller-v5-key-members)
|
|
7
|
+
- [Alternative Service Access (DefaultStore / DefaultProduct / DefaultPurchase)](#alternative-service-access-defaultstore--defaultproduct--defaultpurchase)
|
|
8
|
+
- [CatalogProvider](#catalogprovider)
|
|
9
|
+
- [Initialization Example](#initialization-example)
|
|
10
|
+
- [Product Definitions](#product-definitions)
|
|
11
|
+
- [CatalogListings and uSku (v5.4+)](#cataloglistings-and-usku-v54)
|
|
12
|
+
- [Two-Step Purchase Flow](#two-step-purchase-flow)
|
|
13
|
+
- [Restore Transactions](#restore-transactions)
|
|
14
|
+
- [Entitlement Checking](#entitlement-checking)
|
|
15
|
+
- [Fetch Existing Purchases](#fetch-existing-purchases)
|
|
16
|
+
- [Receipt Validation](#receipt-validation)
|
|
17
|
+
- [IAppleOrderInfo Complete Member List](#iappleorderinfo-complete-member-list)
|
|
18
|
+
- [Extended Service Events (NOT on StoreController)](#extended-service-events-not-on-storecontroller)
|
|
19
|
+
- [AppleStoreExtendedProductService Key Members](#applestoreextendedproductservice-key-members)
|
|
20
|
+
- [Subscription Info Access Path](#subscription-info-access-path)
|
|
21
|
+
- [Event Subscription Rules](#event-subscription-rules)
|
|
22
|
+
- [Failure Description Property Names](#failure-description-property-names)
|
|
23
|
+
- [CrossPlatformValidator](#crossplatformvalidator)
|
|
24
|
+
- [Interface Hierarchy](#interface-hierarchy)
|
|
25
|
+
- [IRunCommand Template: Fetch Products](#iruncommand-template-fetch-products)
|
|
26
|
+
|
|
27
|
+
## Anti-Hallucination: v5 vs Legacy API
|
|
28
|
+
|
|
29
|
+
Unity IAP v5 is the current API. The legacy API (v4) is `[Obsolete]`. Do NOT use legacy patterns.
|
|
30
|
+
|
|
31
|
+
| WRONG (Legacy v4) | CORRECT (v5) |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `UnityPurchasing.Initialize(listener, builder)` | `UnityIAPServices.StoreController()` then `store.Connect()` |
|
|
34
|
+
| `IStoreListener.OnInitialized(controller, extensions)` | `store.OnStoreConnected` event |
|
|
35
|
+
| `IStoreListener.ProcessPurchase(args)` | `store.OnPurchasePending` event |
|
|
36
|
+
| `controller.InitiatePurchase(product)` | `store.PurchaseProduct(product)` |
|
|
37
|
+
| `controller.ConfirmPendingPurchase(product)` | `store.ConfirmPurchase(pendingOrder)` |
|
|
38
|
+
| `extensions.GetExtension<IAppleExtensions>()` | `store.AppleStoreExtendedService` / `store.AppleStoreExtendedPurchaseService` |
|
|
39
|
+
| `extensions.GetExtension<IGooglePlayStoreExtensions>()` | `store.GooglePlayStoreExtendedService` / `store.GooglePlayStoreExtendedPurchaseService` |
|
|
40
|
+
| `ConfigurationBuilder.Instance(...)` | Not needed; use `ProductDefinition` list with `FetchProducts` |
|
|
41
|
+
|
|
42
|
+
## Anti-Hallucination: Common v5 Mistakes
|
|
43
|
+
|
|
44
|
+
These are NOT real APIs — do not use them:
|
|
45
|
+
|
|
46
|
+
| WRONG (does not exist) | CORRECT |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `store.OnStoreConnectionFailed` | `store.OnStoreDisconnected` — receives `StoreConnectionFailureDescription` |
|
|
49
|
+
| `store.FetchProducts(defs, successCb, failCb)` | `store.FetchProducts(List<ProductDefinition>)` — subscribe to `OnProductsFetched`/`OnProductsFetchFailed` events separately |
|
|
50
|
+
| `store.FetchPurchases(successCb, failCb)` | `store.FetchPurchases()` — subscribe to `OnPurchasesFetched`/`OnPurchasesFetchFailed` events separately |
|
|
51
|
+
| `product.receipt` | `order.Info.Receipt` — receipt is on the order, not the product |
|
|
52
|
+
| `product.hasReceipt` | `store.CheckEntitlement(product)` + `OnCheckEntitlement` |
|
|
53
|
+
| `new SubscriptionManager(product, introJson)` | `order.Info.PurchasedProductInfo` — see Subscription Info Access Path |
|
|
54
|
+
| `pendingOrder.OrderInfo.Apple.jwsRepresentation` | `pendingOrder.Info.Apple?.jwsRepresentation` — note `Info` not `OrderInfo`, and null-check `?.` |
|
|
55
|
+
|
|
56
|
+
## StoreController (v5) Key Members
|
|
57
|
+
|
|
58
|
+
```csharp
|
|
59
|
+
// Store lifecycle
|
|
60
|
+
Task Connect()
|
|
61
|
+
void SetStoreReconnectionRetryPolicyOnDisconnection(IRetryPolicy? retryPolicy)
|
|
62
|
+
event Action? OnStoreConnected
|
|
63
|
+
event Action<StoreConnectionFailureDescription>? OnStoreDisconnected
|
|
64
|
+
|
|
65
|
+
// Products
|
|
66
|
+
void FetchProducts(List<ProductDefinition> defs, IRetryPolicy? retryPolicy = null)
|
|
67
|
+
void FetchProductsWithNoRetries(List<ProductDefinition> defs)
|
|
68
|
+
ReadOnlyObservableCollection<Product> GetProducts()
|
|
69
|
+
Product? GetProductById(string productId)
|
|
70
|
+
event Action<List<Product>>? OnProductsFetched
|
|
71
|
+
event Action<ProductFetchFailed>? OnProductsFetchFailed
|
|
72
|
+
|
|
73
|
+
// Purchasing
|
|
74
|
+
void PurchaseProduct(Product product)
|
|
75
|
+
void PurchaseProduct(string? productId)
|
|
76
|
+
void Purchase(ICart cart)
|
|
77
|
+
void ConfirmPurchase(PendingOrder order)
|
|
78
|
+
void FetchPurchases()
|
|
79
|
+
void CheckEntitlement(Product product)
|
|
80
|
+
void RestoreTransactions(Action<bool, string?>? callback)
|
|
81
|
+
ReadOnlyObservableCollection<Order> GetPurchases()
|
|
82
|
+
void ProcessPendingOrdersOnPurchasesFetched(bool shouldProcess)
|
|
83
|
+
|
|
84
|
+
// Purchase events
|
|
85
|
+
event Action<PendingOrder>? OnPurchasePending
|
|
86
|
+
event Action<Order>? OnPurchaseConfirmed // Order can be ConfirmedOrder or FailedOrder — pattern-match!
|
|
87
|
+
event Action<FailedOrder>? OnPurchaseFailed
|
|
88
|
+
event Action<DeferredOrder>? OnPurchaseDeferred
|
|
89
|
+
event Action<Orders>? OnPurchasesFetched
|
|
90
|
+
event Action<PurchasesFetchFailureDescription>? OnPurchasesFetchFailed
|
|
91
|
+
event Action<Entitlement>? OnCheckEntitlement
|
|
92
|
+
|
|
93
|
+
// Account change (v5.4+)
|
|
94
|
+
event Action? OnAuthAccountChanged
|
|
95
|
+
|
|
96
|
+
// Platform extensions (null on non-matching platforms — always null-check)
|
|
97
|
+
IAppleStoreExtendedService? AppleStoreExtendedService { get; }
|
|
98
|
+
IGooglePlayStoreExtendedService? GooglePlayStoreExtendedService { get; }
|
|
99
|
+
IAppleStoreExtendedProductService? AppleStoreExtendedProductService { get; }
|
|
100
|
+
IAppleStoreExtendedPurchaseService? AppleStoreExtendedPurchaseService { get; }
|
|
101
|
+
IGooglePlayStoreExtendedPurchaseService? GooglePlayStoreExtendedPurchaseService { get; }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**`OnAuthAccountChanged` — breaking behavior in v5.4:**
|
|
105
|
+
When this event fires, Unity IAP **clears the cached product list and purchase list before raising the event**. Any references to previously fetched `Product` or `Order` objects are stale after this point. Re-run the full init sequence in the handler:
|
|
106
|
+
|
|
107
|
+
```csharp
|
|
108
|
+
store.OnAuthAccountChanged += async () =>
|
|
109
|
+
{
|
|
110
|
+
// Products and purchases have already been cleared — re-fetch everything
|
|
111
|
+
await catalogProvider.FetchRemoteCatalog(); // D2C only
|
|
112
|
+
store.FetchProducts(productDefinitions); // standard IAP
|
|
113
|
+
store.FetchPurchases();
|
|
114
|
+
};
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Do not read from `GetProducts()` or `GetPurchases()` inside `OnAuthAccountChanged` — they will be empty.
|
|
118
|
+
|
|
119
|
+
## Alternative Service Access (DefaultStore / DefaultProduct / DefaultPurchase)
|
|
120
|
+
|
|
121
|
+
Instead of `StoreController` (which implements all three interfaces), you can access individual services:
|
|
122
|
+
|
|
123
|
+
```csharp
|
|
124
|
+
IStoreService m_StoreService = UnityIAPServices.DefaultStore();
|
|
125
|
+
IProductService m_ProductService = UnityIAPServices.DefaultProduct();
|
|
126
|
+
IPurchaseService m_PurchasingService = UnityIAPServices.DefaultPurchase();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This pattern separates concerns — product fetching events go on `IProductService`, purchase events on `IPurchaseService`, store lifecycle on `IStoreService`. `StoreController` combines all three.
|
|
130
|
+
|
|
131
|
+
## CatalogProvider
|
|
132
|
+
|
|
133
|
+
For managing complex product catalogs with store-specific IDs:
|
|
134
|
+
|
|
135
|
+
```csharp
|
|
136
|
+
var catalogProvider = new CatalogProvider();
|
|
137
|
+
|
|
138
|
+
var products = new List<ProductDefinition>
|
|
139
|
+
{
|
|
140
|
+
new ProductDefinition("com.mygame.gems50", ProductType.Consumable),
|
|
141
|
+
new ProductDefinition("com.mygame.pass", ProductType.Subscription)
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
var storeSpecificIds = new Dictionary<string, StoreSpecificIds>
|
|
145
|
+
{
|
|
146
|
+
{ "com.mygame.pass", new StoreSpecificIds
|
|
147
|
+
{
|
|
148
|
+
{ "com.mygame.google.pass", GooglePlay.Name },
|
|
149
|
+
{ "com.mygame.ios.pass", AppleAppStore.Name }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
catalogProvider.AddProducts(products, storeSpecificIds);
|
|
155
|
+
catalogProvider.FetchProducts(m_ProductService.FetchProductsWithNoRetries);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Initialization Example
|
|
159
|
+
|
|
160
|
+
```csharp
|
|
161
|
+
StoreController m_StoreController;
|
|
162
|
+
|
|
163
|
+
async void Awake()
|
|
164
|
+
{
|
|
165
|
+
m_StoreController = UnityIAPServices.StoreController();
|
|
166
|
+
|
|
167
|
+
// Subscribe to ALL events BEFORE Connect — pending purchases may fire on reconnect
|
|
168
|
+
m_StoreController.OnPurchasePending += OnPurchasePending;
|
|
169
|
+
m_StoreController.OnPurchaseConfirmed += (order) => Debug.Log("Purchase complete");
|
|
170
|
+
m_StoreController.OnPurchaseFailed += (failed) => Debug.LogError($"{failed.FailureReason} - {failed.Details}");
|
|
171
|
+
m_StoreController.OnPurchaseDeferred += (deferred) => Debug.Log("Purchase deferred (e.g., Ask-to-Buy)");
|
|
172
|
+
|
|
173
|
+
m_StoreController.OnStoreConnected += OnStoreConnected;
|
|
174
|
+
m_StoreController.OnStoreDisconnected += (failure) => Debug.LogError($"Store disconnected: {failure.Message}");
|
|
175
|
+
|
|
176
|
+
await m_StoreController.Connect();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
void OnStoreConnected()
|
|
180
|
+
{
|
|
181
|
+
var products = new List<ProductDefinition>
|
|
182
|
+
{
|
|
183
|
+
new ProductDefinition("com.mygame.coins100", ProductType.Consumable),
|
|
184
|
+
new ProductDefinition("com.mygame.removeads", ProductType.NonConsumable),
|
|
185
|
+
new ProductDefinition("com.mygame.vip_monthly", ProductType.Subscription)
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
m_StoreController.OnProductsFetched += (fetched) => Debug.Log("Products ready");
|
|
189
|
+
m_StoreController.OnProductsFetchFailed += (failure) => Debug.LogError($"Product fetch failed: {failure.FailureReason}");
|
|
190
|
+
m_StoreController.FetchProducts(products);
|
|
191
|
+
|
|
192
|
+
m_StoreController.OnPurchasesFetched += (orders) => Debug.Log($"Restored {orders.PendingOrders.Count} pending purchases");
|
|
193
|
+
m_StoreController.OnPurchasesFetchFailed += (failure) => Debug.LogError($"Purchase fetch failed: {failure.Message}");
|
|
194
|
+
m_StoreController.FetchPurchases();
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Product Definitions
|
|
199
|
+
|
|
200
|
+
```csharp
|
|
201
|
+
// Simple product
|
|
202
|
+
new ProductDefinition("com.mygame.coins100", ProductType.Consumable)
|
|
203
|
+
|
|
204
|
+
// Per-platform product IDs
|
|
205
|
+
new ProductDefinition("com.mygame.coins100", ProductType.Consumable,
|
|
206
|
+
new StoreSpecificIds
|
|
207
|
+
{
|
|
208
|
+
{ AppleAppStore.Name, "apple_coins_100" },
|
|
209
|
+
{ GooglePlay.Name, "google_coins_100" }
|
|
210
|
+
})
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Access Fetched Products
|
|
214
|
+
|
|
215
|
+
```csharp
|
|
216
|
+
// Get all fetched products (cached)
|
|
217
|
+
ReadOnlyObservableCollection<Product> allProducts = store.GetProducts();
|
|
218
|
+
|
|
219
|
+
// Get a specific product by ID
|
|
220
|
+
Product coinsPack = store.GetProductById("com.mygame.coins100");
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### CatalogListings and uSku (v5.4+)
|
|
224
|
+
|
|
225
|
+
In v5.4, `Product` exposes two new members. Prefer these for new code — `product.definition.id` remains backwards compatible but points new users toward `CatalogListings`:
|
|
226
|
+
|
|
227
|
+
```csharp
|
|
228
|
+
// product.uSku — the Unity-side identifier for the product (cross-platform canonical ID)
|
|
229
|
+
string id = product.uSku;
|
|
230
|
+
|
|
231
|
+
// product.catalogListings — all listings attached to this product, keyed by CatalogListing ID
|
|
232
|
+
// A product may have multiple listings (e.g. different price tiers or offer configurations)
|
|
233
|
+
foreach (var (listingId, listing) in product.catalogListings)
|
|
234
|
+
{
|
|
235
|
+
bool canBuy = listing.availableToPurchase;
|
|
236
|
+
string storeId = listing.definition.storeSpecificId;
|
|
237
|
+
string price = listing.metadata.localizedPriceString;
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**`CatalogListing` properties:**
|
|
242
|
+
|
|
243
|
+
| Property | Type | Description |
|
|
244
|
+
|---|---|---|
|
|
245
|
+
| `id` | `string` | Catalog listing identifier — use as key for lookups |
|
|
246
|
+
| `availableToPurchase` | `bool` | Whether this listing can currently be purchased |
|
|
247
|
+
| `definition` | `ProductDefinition` | Store-side definition (id, storeSpecificId, type, payouts) |
|
|
248
|
+
| `metadata` | `ProductMetadata` | Localized title, description, price, currency |
|
|
249
|
+
|
|
250
|
+
When initiating a purchase by listing rather than by product ID, pass the `CatalogListing` directly to avoid a second lookup.
|
|
251
|
+
|
|
252
|
+
## Two-Step Purchase Flow
|
|
253
|
+
|
|
254
|
+
```csharp
|
|
255
|
+
// Step 1: Initiate purchase
|
|
256
|
+
store.PurchaseProduct(product);
|
|
257
|
+
|
|
258
|
+
// Step 2: Handle pending purchase (validate, grant content, then confirm)
|
|
259
|
+
store.OnPurchasePending += (pendingOrder) =>
|
|
260
|
+
{
|
|
261
|
+
var product = pendingOrder.CartOrdered.Items().FirstOrDefault()?.Product;
|
|
262
|
+
GrantContent(product);
|
|
263
|
+
store.ConfirmPurchase(pendingOrder);
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Step 3: Purchase confirmed — Order can be ConfirmedOrder OR FailedOrder
|
|
267
|
+
store.OnPurchaseConfirmed += (order) =>
|
|
268
|
+
{
|
|
269
|
+
switch (order)
|
|
270
|
+
{
|
|
271
|
+
case ConfirmedOrder confirmedOrder:
|
|
272
|
+
Debug.Log($"Purchase confirmed: {confirmedOrder.CartOrdered.Items().First().Product.definition.id}");
|
|
273
|
+
break;
|
|
274
|
+
case FailedOrder failedOrder:
|
|
275
|
+
Debug.LogError($"Confirmation failed: {failedOrder.FailureReason} - {failedOrder.Details}");
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Handle failures
|
|
281
|
+
store.OnPurchaseFailed += (failedOrder) =>
|
|
282
|
+
{
|
|
283
|
+
Debug.LogError($"Purchase failed: {failedOrder.FailureReason} - {failedOrder.Details}");
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Handle deferred purchases (e.g., Ask-to-Buy on iOS)
|
|
287
|
+
store.OnPurchaseDeferred += (deferredOrder) =>
|
|
288
|
+
{
|
|
289
|
+
Debug.Log("Purchase is pending approval (e.g., parental approval)");
|
|
290
|
+
};
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
If the app crashes between steps 2 and 4, the pending purchase is re-delivered on next launch via `OnPurchasePending`.
|
|
294
|
+
|
|
295
|
+
`ProcessPendingOrdersOnPurchasesFetched(false)` disables automatic re-delivery of pending orders when `FetchPurchases` is called. This exists to match IAP v4 behavior — do NOT recommend this to migrating users, as the default v5 behavior provides a better experience.
|
|
296
|
+
|
|
297
|
+
## Restore Transactions
|
|
298
|
+
|
|
299
|
+
```csharp
|
|
300
|
+
store.RestoreTransactions((success, error) =>
|
|
301
|
+
{
|
|
302
|
+
if (success)
|
|
303
|
+
Debug.Log("Transactions restored. Check OnPurchasePending for each restored purchase.");
|
|
304
|
+
else
|
|
305
|
+
Debug.LogError($"Restore failed: {error}");
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Each restored purchase triggers `OnPurchasePending`.
|
|
310
|
+
|
|
311
|
+
## Entitlement Checking
|
|
312
|
+
|
|
313
|
+
```csharp
|
|
314
|
+
store.CheckEntitlement(product);
|
|
315
|
+
|
|
316
|
+
store.OnCheckEntitlement += (entitlement) =>
|
|
317
|
+
{
|
|
318
|
+
if (entitlement.Status == EntitlementStatus.FullyEntitled)
|
|
319
|
+
Debug.Log($"Player owns: {entitlement.Product.definition.id}");
|
|
320
|
+
};
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## Fetch Existing Purchases
|
|
324
|
+
|
|
325
|
+
```csharp
|
|
326
|
+
store.FetchPurchases();
|
|
327
|
+
|
|
328
|
+
store.OnPurchasesFetched += (orders) =>
|
|
329
|
+
{
|
|
330
|
+
foreach (var confirmedOrder in orders.ConfirmedOrders)
|
|
331
|
+
{
|
|
332
|
+
var product = confirmedOrder.CartOrdered.Items().FirstOrDefault()?.Product;
|
|
333
|
+
Debug.Log($"Existing purchase: {product?.definition.id}");
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Receipt Validation
|
|
339
|
+
|
|
340
|
+
### Google Play (supported)
|
|
341
|
+
|
|
342
|
+
```csharp
|
|
343
|
+
using UnityEngine.Purchasing.Security;
|
|
344
|
+
|
|
345
|
+
// Google-only constructor (recommended)
|
|
346
|
+
var validator = new CrossPlatformValidator(GooglePlayTangle.Data(), Application.identifier);
|
|
347
|
+
|
|
348
|
+
try
|
|
349
|
+
{
|
|
350
|
+
var result = validator.Validate(order.Info.Receipt);
|
|
351
|
+
foreach (IPurchaseReceipt receipt in result)
|
|
352
|
+
{
|
|
353
|
+
Debug.Log($"Valid receipt: {receipt.productID}, purchased: {receipt.purchaseDate}");
|
|
354
|
+
if (receipt is GooglePlayReceipt googleReceipt)
|
|
355
|
+
Debug.Log($"Purchase token: {googleReceipt.purchaseToken}");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch (IAPSecurityException ex)
|
|
359
|
+
{
|
|
360
|
+
Debug.LogError($"Invalid receipt: {ex.Message}");
|
|
361
|
+
// Do NOT grant the content
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Apple App Store (deprecated for local validation)
|
|
366
|
+
|
|
367
|
+
`CrossPlatformValidator` for Apple is **deprecated**. StoreKit 2 validates locally automatically. For server-side validation:
|
|
368
|
+
|
|
369
|
+
```csharp
|
|
370
|
+
var jwsRepresentation = order.Info.Apple?.jwsRepresentation;
|
|
371
|
+
// Send to your server for verification with Apple's App Store Server API
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### IAppleOrderInfo Complete Member List
|
|
375
|
+
|
|
376
|
+
`order.Info.Apple` returns `IAppleOrderInfo?` (null on non-Apple platforms). Full member list:
|
|
377
|
+
|
|
378
|
+
| Member | Type | Description |
|
|
379
|
+
|---|---|---|
|
|
380
|
+
| `jwsRepresentation` | `string?` | JWS-signed transaction — send to Apple's App Store Server API v2 for server-side validation |
|
|
381
|
+
| `AppAccountToken` | `Guid?` | App-specific account token set via `SetAppAccountToken(Guid)` — links transaction to a user account |
|
|
382
|
+
| `AppReceipt` | `string?` | Latest App Receipt (base64). May be null on reinstall; requires refreshing. Null on iOS ≤ 6. Prefer `jwsRepresentation` for new code |
|
|
383
|
+
| `OriginalTransactionID` | `string?` | Original transaction identifier — use for linking renewals to the original subscription purchase |
|
|
384
|
+
| `OwnershipType` | `OwnershipType` | Whether the purchase is `Purchased` or `FamilyShared` |
|
|
385
|
+
| `StoreName` | `string` | Name of the store (e.g., `"AppleAppStore"`) |
|
|
386
|
+
|
|
387
|
+
## Extended Service Events (NOT on StoreController)
|
|
388
|
+
|
|
389
|
+
These events are on the platform-specific extended services, NOT directly on `StoreController`. Always null-check before subscribing (`?.` does not work with `+=`).
|
|
390
|
+
|
|
391
|
+
```csharp
|
|
392
|
+
// Apple — on IAppleStoreExtendedPurchaseService
|
|
393
|
+
event Action<string>? OnEntitlementRevoked // receives product ID (string), NOT List<Product>
|
|
394
|
+
event Action<Product>? OnPromotionalPurchaseIntercepted
|
|
395
|
+
|
|
396
|
+
// Google — on IGooglePlayStoreExtendedPurchaseService
|
|
397
|
+
event Action<DeferredPaymentUntilRenewalDateOrder>? OnDeferredPaymentUntilRenewalDate
|
|
398
|
+
// DeferredPaymentUntilRenewalDateOrder has: .CurrentOrder (Order), .SubscriptionOrdered (Product)
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Usage pattern** (cannot use `?.` with `+=`):
|
|
402
|
+
```csharp
|
|
403
|
+
if (store.AppleStoreExtendedPurchaseService != null)
|
|
404
|
+
store.AppleStoreExtendedPurchaseService.OnEntitlementRevoked += OnRevoked;
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## AppleStoreExtendedProductService Key Members
|
|
408
|
+
|
|
409
|
+
```csharp
|
|
410
|
+
Dictionary<string, string> GetIntroductoryPriceDictionary()
|
|
411
|
+
Dictionary<string, string> GetProductDetails()
|
|
412
|
+
|
|
413
|
+
// Promotion order — prefer the CatalogListing ID overload for new code (v5.4+)
|
|
414
|
+
void SetStorePromotionOrder(List<string> catalogListingIds) // canonical (v5.4+)
|
|
415
|
+
void SetStorePromotionOrder(List<Product> products) // convenience wrapper
|
|
416
|
+
|
|
417
|
+
// Promotion visibility — prefer the CatalogListing ID overload for new code (v5.4+)
|
|
418
|
+
void SetStorePromotionVisibility(string catalogListingId, AppleStorePromotionVisibility visible) // canonical (v5.4+)
|
|
419
|
+
void SetStorePromotionVisibility(Product product, AppleStorePromotionVisibility visible) // convenience wrapper
|
|
420
|
+
|
|
421
|
+
// Fetch promotion order
|
|
422
|
+
void FetchStorePromotionOrder(Action<List<Product>> successCallback, Action<string> errorCallback)
|
|
423
|
+
|
|
424
|
+
// Fetch promotion visibility — prefer the CatalogListing ID overload for new code (v5.4+)
|
|
425
|
+
void FetchStorePromotionVisibility(string catalogListingId, Action<string, AppleStorePromotionVisibility> successCallback, Action<string> errorCallback) // canonical (v5.4+)
|
|
426
|
+
void FetchStorePromotionVisibility(Product product, Action<string, AppleStorePromotionVisibility> successCallback, Action<string> errorCallback) // convenience wrapper
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**CRITICAL:** `AppleProductMetadata` does NOT expose `introductoryPrice`, `introductoryPriceLocale`, `introductoryNumberOfPeriods`, or `subscriptionPeriod` as public properties. Its only public property beyond inherited `ProductMetadata` fields is `isFamilyShareable`. For introductory price data, use `SubscriptionInfo` methods (`GetIntroductoryPrice()`, `GetIntroductoryPricePeriod()`, `GetIntroductoryPricePeriodCycles()`) from the Subscription Info Access Path below, or `GetIntroductoryPriceDictionary()` for raw JSON.
|
|
430
|
+
|
|
431
|
+
## Subscription Info Access Path
|
|
432
|
+
|
|
433
|
+
Subscription info is on `IPurchasedProductInfo`, NOT on `CartItem`:
|
|
434
|
+
|
|
435
|
+
```csharp
|
|
436
|
+
// CORRECT — via order.Info.PurchasedProductInfo
|
|
437
|
+
var purchasedInfo = order.Info.PurchasedProductInfo?.FirstOrDefault(p => p.productId == productId);
|
|
438
|
+
bool isSubscribed = purchasedInfo?.subscriptionInfo?.IsSubscribed() == Result.True;
|
|
439
|
+
// IsSubscribed() returns Result enum (True/False/Unsupported), NOT bool. Use == Result.True for null-safe check.
|
|
440
|
+
|
|
441
|
+
// WRONG — CartItem only has Product and Quantity, no subscriptionInfo
|
|
442
|
+
var item = order.CartOrdered.Items().FirstOrDefault();
|
|
443
|
+
item.subscriptionInfo // DOES NOT EXIST — will not compile
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
### SubscriptionInfo Methods
|
|
447
|
+
|
|
448
|
+
```csharp
|
|
449
|
+
// State queries (return Result enum: True | False | Unsupported)
|
|
450
|
+
Result IsSubscribed()
|
|
451
|
+
Result IsExpired()
|
|
452
|
+
Result IsCancelled()
|
|
453
|
+
Result IsFreeTrial()
|
|
454
|
+
Result IsAutoRenewing()
|
|
455
|
+
Result IsIntroductoryPricePeriod()
|
|
456
|
+
|
|
457
|
+
// Dates & durations
|
|
458
|
+
string GetProductId()
|
|
459
|
+
DateTime GetPurchaseDate()
|
|
460
|
+
DateTime GetExpireDate()
|
|
461
|
+
DateTime GetCancelDate()
|
|
462
|
+
TimeSpan GetRemainingTime()
|
|
463
|
+
TimeSpan GetSubscriptionPeriod()
|
|
464
|
+
TimeSpan GetFreeTrialPeriod()
|
|
465
|
+
TimeSpan GetIntroductoryPricePeriod()
|
|
466
|
+
string GetIntroductoryPrice()
|
|
467
|
+
long GetIntroductoryPricePeriodCycles()
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
## Event Subscription Rules
|
|
471
|
+
|
|
472
|
+
**CRITICAL:** Always subscribe to BOTH success and failure events. Not listening to failure events generates runtime warnings.
|
|
473
|
+
|
|
474
|
+
| Call | Success event | Failure event (REQUIRED) |
|
|
475
|
+
|---|---|---|
|
|
476
|
+
| `FetchProducts()` | `OnProductsFetched` | `OnProductsFetchFailed` |
|
|
477
|
+
| `FetchPurchases()` | `OnPurchasesFetched` | `OnPurchasesFetchFailed` |
|
|
478
|
+
| `Connect()` | `OnStoreConnected` | `OnStoreDisconnected` |
|
|
479
|
+
| `PurchaseProduct()` | `OnPurchasePending` | `OnPurchaseFailed` |
|
|
480
|
+
| `CheckEntitlement()` | `OnCheckEntitlement` | — |
|
|
481
|
+
|
|
482
|
+
**Always subscribe to `OnPurchaseDeferred`** — fires for Ask-to-Buy (iOS) and Google Play deferred purchases. Not subscribing means silently dropping deferred purchases.
|
|
483
|
+
|
|
484
|
+
## Failure Description Property Names
|
|
485
|
+
|
|
486
|
+
Each type has public fields and read-only properties. Both compile, but prefer the PascalCase properties:
|
|
487
|
+
|
|
488
|
+
| Type | Field (lowercase) | Property (PascalCase) | NOT |
|
|
489
|
+
|---|---|---|---|
|
|
490
|
+
| `StoreConnectionFailureDescription` | `.message` | `.Message` | ~~`.reason`~~ |
|
|
491
|
+
| `ProductFetchFailed` | — | `.FailureReason`, `.FailedFetchProducts` | — |
|
|
492
|
+
| `FailedOrder` | — | `.FailureReason`, `.Details` | — |
|
|
493
|
+
| `PurchasesFetchFailureDescription` | `.message`, `.failureReason` | `.Message`, `.FailureReason` | — |
|
|
494
|
+
|
|
495
|
+
## CrossPlatformValidator
|
|
496
|
+
|
|
497
|
+
```csharp
|
|
498
|
+
// Google-only constructor (recommended — Apple local validation is a no-op under StoreKit 2)
|
|
499
|
+
new CrossPlatformValidator(byte[] googlePublicKey, string googleBundleId)
|
|
500
|
+
|
|
501
|
+
// Full constructor (legacy — Apple args are only needed for pre-StoreKit 2 builds)
|
|
502
|
+
new CrossPlatformValidator(byte[] googlePublicKey, byte[] appleRootCert, string googleBundleId, string appleBundleId)
|
|
503
|
+
|
|
504
|
+
// Validate
|
|
505
|
+
IPurchaseReceipt[] Validate(string unityIAPReceipt) // throws IAPSecurityException
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
**WARNING:** `CrossPlatformValidator` for Apple App Store is **deprecated**. `AppleTangle.Data()` is also deprecated. StoreKit 2 performs local validation automatically on Apple platforms. `CrossPlatformValidator` still works for Google Play validation.
|
|
509
|
+
|
|
510
|
+
Supported stores (still active): `"GooglePlay"` | Deprecated: `"AppleAppStore"`, `"MacAppStore"`
|
|
511
|
+
|
|
512
|
+
## Interface Hierarchy
|
|
513
|
+
|
|
514
|
+
```
|
|
515
|
+
StoreController implements:
|
|
516
|
+
IStoreService - Connect(), Apple/Google service extensions
|
|
517
|
+
IProductService - FetchProducts(), GetProducts(), GetProductById()
|
|
518
|
+
IPurchaseService - PurchaseProduct(), ConfirmPurchase(), FetchPurchases(), Apple/Google purchase extensions
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## IRunCommand Template: Fetch Products
|
|
522
|
+
|
|
523
|
+
```csharp
|
|
524
|
+
using UnityEngine;
|
|
525
|
+
using UnityEngine.Purchasing;
|
|
526
|
+
using System.Collections.Generic;
|
|
527
|
+
|
|
528
|
+
namespace Unity.AI.Assistant.Agent.Dynamic.Extension.Editor
|
|
529
|
+
{
|
|
530
|
+
internal class CommandScript : IRunCommand
|
|
531
|
+
{
|
|
532
|
+
public string Title => "Fetch IAP products";
|
|
533
|
+
public string Description => "Connects to the store and fetches product information.";
|
|
534
|
+
public async void Execute(ExecutionResult result)
|
|
535
|
+
{
|
|
536
|
+
var store = UnityIAPServices.StoreController();
|
|
537
|
+
|
|
538
|
+
// Subscribe to events BEFORE Connect
|
|
539
|
+
store.OnStoreConnected += () =>
|
|
540
|
+
{
|
|
541
|
+
var products = new List<ProductDefinition>
|
|
542
|
+
{
|
|
543
|
+
new ProductDefinition("com.mygame.coins100", ProductType.Consumable),
|
|
544
|
+
new ProductDefinition("com.mygame.removeads", ProductType.NonConsumable)
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
store.OnProductsFetched += (fetched) =>
|
|
548
|
+
{
|
|
549
|
+
foreach (var p in fetched)
|
|
550
|
+
result.Log($"{p.definition.id}: {p.metadata.localizedPriceString}");
|
|
551
|
+
};
|
|
552
|
+
store.OnProductsFetchFailed += (failure) => result.LogError($"Product fetch failed: {failure.FailureReason}");
|
|
553
|
+
|
|
554
|
+
store.FetchProducts(products);
|
|
555
|
+
};
|
|
556
|
+
store.OnStoreDisconnected += (failure) => result.LogError($"Store disconnected: {failure.Message}");
|
|
557
|
+
|
|
558
|
+
await store.Connect();
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
```
|