Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 106 additions & 98 deletions skills/implement-in-app-purchases/SKILL.md

Large diffs are not rendered by default.

566 changes: 56 additions & 510 deletions skills/implement-in-app-purchases/references/api-notes.md

Large diffs are not rendered by default.

204 changes: 35 additions & 169 deletions skills/implement-in-app-purchases/references/codeless-catalog.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Adapty — Conversion Assessment and Guidance

Links to other reference files point to files in this `references/` folder. Any other relative paths resolve to the skill directory.

## Table of Contents

- [Trigger Phrases](#trigger-phrases)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# RevenueCat — Conversion Assessment and Guidance

Links to other reference files point to files in this `references/` folder. Any other relative paths resolve to the skill directory.

## Table of Contents

- [Trigger Phrases](#trigger-phrases)
Expand Down
220 changes: 31 additions & 189 deletions skills/implement-in-app-purchases/references/migration-v4-to-v5.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,16 @@ Create a single `IAPManager` (MonoBehaviour singleton or service, matching proje
- Exposes `Buy(string productId)`.
- Exposes `RestorePurchases()` — **only** if the product list contains `NonConsumable` or `Subscription` types.
- Fires UI-friendly events or callbacks for: `OnInitialized`, `OnProductsLoaded`, `OnPurchaseSuccess`, `OnPurchaseFailed`, `OnPurchaseDeferred`.
- Initializes once in `Awake()` — see the **Initialization Flow** section in SKILL.md for the exact event subscription and `Connect()` ordering rules.
- Initializes once in `Awake()` — see **Core rules > Initialization** in the parent skill file for the event subscription and `Connect()` ordering rules.
- Registers all products from a single authoritative product list (not scattered across UI handlers).

### Product catalog

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.
Define products in one place, never inside button click handlers, so IDs can't drift between the shop UI and the store. By default, use a static class that holds the product ID constants and builds the `List<ProductDefinition>` passed to `FetchProducts()`. Use a `ScriptableObject` instead only if the project already configures game data through ScriptableObjects, or the user wants designers to edit products in the Inspector. Wire button click handlers to `IAPManager.Buy(productId)` using those constants, not hardcoded strings.

## Step 5 — Purchase Handling Contract

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.
For API mechanics (two-step flow, event names, `ConfirmPurchase` signature) read the [Purchases](https://docs.unity.com/en-us/iap/purchases.md) page and **Core rules > Purchases** in the parent skill file. This section covers only the grant-and-save contract that is specific to this path.

### PendingOrder — the save-before-confirm rule

Expand All @@ -120,7 +120,7 @@ Do not grant anything. Fire `OnPurchaseDeferred` event to update UI ("Purchase p

### FailedOrder

Do not grant anything. Fire `OnPurchaseFailed` with the reason. See **Failure Description Property Names** in SKILL.md for the correct property names per type.
Do not grant anything. Fire `OnPurchaseFailed` with the reason. See **Core rules > Compile errors** in the parent skill file for the correct property names per type.

## Step 6 — Product Type Behavior Rules

Expand All @@ -135,13 +135,13 @@ Do not grant anything. Fire `OnPurchaseFailed` with the reason. See **Failure De

- Unlock a durable entitlement (feature flag, item ownership).
- Include `RestorePurchases()` — required on iOS, good practice on Android.
- 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.
- Re-apply the entitlement on app startup: call `store.FetchPurchases()` and re-check ownership in `OnPurchasesFetched`, or use `store.CheckEntitlement(product)`. See **Core rules > Ownership and subscriptions** in the parent skill file.

### Subscription

- Restore or check subscription state on app startup, not only at purchase time. Subscriptions can expire or be cancelled externally.
- Use `store.CheckEntitlement(product)` or inspect `OnPurchasesFetched` results to update active/expired/unknown state.
- See **Subscription Info** in SKILL.md for the correct access path (`order.Info.PurchasedProductInfo`, `IsSubscribed() == Result.True`).
- See **Core rules > Ownership and subscriptions** in the parent skill file for the correct access path (`order.Info.PurchasedProductInfo`, `IsSubscribed() == Result.True`).

## Step 7 — Cloud Save Integration

Expand All @@ -156,9 +156,8 @@ SaveGame\(|SaveDataAsync\(|PlayerPrefs\.SetInt|JsonUtility\.ToJson|JsonConvert\.

### Rules

- **Use the existing save abstraction** — do not create a new save system.
- Prefer methods already in use: `CloudSaveService.Instance.Data.Player.SaveAsync()`, `SaveGame()`, `SaveDataAsync()`, or equivalent.
- If no save system exists, use `PlayerPrefs` as a minimal fallback and document it as a TODO for the developer to upgrade.
- **Use the existing save abstraction** — do not create a new save system. If the project has more than one, use the one that already stores the currency or inventory the purchase credits, so the grant and the balance are saved together.
- If no save system exists, use `PlayerPrefs` as the fallback, because it needs no new package or backend, and list it as a follow-up in the report, since `PlayerPrefs` is local to the device and not safe against tampering.
- Save **must complete before `ConfirmPurchase`** is called (see Step 5).

## Step 8 — UI Integration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Convert Native Google Billing to Unity IAP 5

Links to other reference files point to files in this `references/` folder. Any other relative paths resolve to the skill directory.

## Table of Contents

- [Trigger Phrases](#trigger-phrases)
Expand Down Expand Up @@ -225,88 +227,32 @@ If game code calls the old billing API directly (e.g., `GoogleBillingManager.Pur

### Phase 6 — Test Scaffolding

- Add debug logs at each IAP event.
- Add editor stubs for sandbox testing.
- Add the QA checklist from the test plan below to `IapMigrationNotes.md`.

## Step 7 — Behavior Change Rules

### Acknowledgement and Consumption

Native billing requires explicit `acknowledgePurchase` or `consumeAsync`. Unity IAP abstracts both through `ConfirmPurchase(pendingOrder)`. Map:

| Native | Unity IAP |
|---|---|
| `consumeAsync` (consumable) | `ConfirmPurchase(pendingOrder)` after grant |
| `acknowledgePurchase` (non-consumable / subscription) | `ConfirmPurchase(pendingOrder)` after grant |

**Never call `ConfirmPurchase` before the entitlement is safely granted.**

### Backend Validation

| Native field sent | Unity IAP equivalent |
|---|---|
| `purchaseToken` | `order.Info.Receipt` (contains the Google receipt JSON) |
| `originalJson` | Embedded in `order.Info.Receipt` |
| `signature` | Embedded in `order.Info.Receipt` |
| `packageName` | `Application.identifier` |
| `productId` | `pendingOrder.CartOrdered.Items().First().Product.definition.id` |

If the backend parses `purchaseToken` or `originalJson` fields directly, document the receipt format change as a required backend update.

### Obfuscated IDs

If native code sets `setObfuscatedAccountId` / `setObfuscatedProfileId` in `BillingFlowParams`, set the equivalent **after** `await store.Connect()`:

```csharp
store.GooglePlayStoreExtendedService?.SetObfuscatedAccountId("hashed_id");
store.GooglePlayStoreExtendedService?.SetObfuscatedProfileId("hashed_profile_id");
```
These are the changes that break games or backends after the switch. For the Unity IAP side of each call, read the docs page named in the Full topic map of the parent skill file.

Retrieve per-order:

```csharp
string accountId = store.GooglePlayStoreExtendedPurchaseService?.GetObfuscatedAccountId(order);
```

### Pending Purchases

If native code handles `BillingClient.enablePendingPurchases()` or checks purchase state for `PENDING`:

- Unity IAP surfaces deferred purchases via `store.OnPurchaseDeferred`.
- On `OnPurchaseDeferred`: show "pending approval" UI. Do not grant.
- When approved, the store re-delivers via `store.OnPurchasePending`.

### Subscription Upgrade / Downgrade

If native code calls `launchBillingFlow` with a `subscriptionUpdateParams` (prorating an existing subscription):

```csharp
store.GooglePlayStoreExtendedPurchaseService?.UpgradeDowngradeSubscription(
currentOrder, // Order object of the current subscription
newProduct, // Product to upgrade/downgrade to
GooglePlayReplacementMode.ChargeFullPrice // choose appropriate mode
);
```

See [platform-notes.md](platform-notes.md) for all `GooglePlayReplacementMode` values.
- **One call replaces acknowledge and consume.** `ConfirmPurchase(pendingOrder)` replaces both `acknowledgePurchase` and `consumeAsync`. Call it only after the entitlement is granted and saved, because a confirmed purchase is never re-delivered.
- **The receipt format changes for the backend.** `purchaseToken`, `originalJson` and `signature` are no longer separate values; they are embedded in `order.Info.Receipt`. If the backend reads them directly, list the receipt format change as a required backend update in `IapMigrationNotes.md`.
- **Obfuscated IDs move after `Connect()`.** Native code sets them on `BillingFlowParams` per purchase. In Unity IAP, set them once on `GooglePlayStoreExtendedService` after `await store.Connect()`, and read them back per order from `GooglePlayStoreExtendedPurchaseService`. See the Google Play notes in [platform-notes.md](platform-notes.md).
- **Pending purchases become deferred orders.** Native `PENDING` purchase state maps to `store.OnPurchaseDeferred`. Show a pending state and don't grant; the approved purchase arrives later through `OnPurchasePending`.
- **Subscription changes take objects, not IDs.** `launchBillingFlow` with `subscriptionUpdateParams` maps to `UpgradeDowngradeSubscription(currentOrder, newProduct, mode)`. Choose the mode as the Google Play notes in [platform-notes.md](platform-notes.md) describe.

## Step 8 — Native → Unity IAP Concept Mapping

Only the mappings that aren't one-to-one renames, or that agents commonly get wrong:

| Native Google Billing | Unity IAP 5.x |
|---|---|
| `ProductDetails` | `Product` (via `store.GetProductById`) |
| `Purchase` | `PendingOrder` (on `OnPurchasePending`) |
| `purchaseToken` | `order.Info.Receipt` (contains the token) |
| `originalJson` | embedded in `order.Info.Receipt` |
| `signature` | embedded in `order.Info.Receipt` |
| `acknowledgePurchase` | `store.ConfirmPurchase(pendingOrder)` |
| `consumeAsync` | `store.ConfirmPurchase(pendingOrder)` |
| `ProductDetails` | `Product`, from `store.GetProductById` after `OnProductsFetched` |
| `Purchase` | `PendingOrder`, from `OnPurchasePending` |
| `queryPurchasesAsync` | `store.FetchPurchases()` → `OnPurchasesFetched` |
| `BillingResult` / `BillingResponseCode` | `FailedOrder.FailureReason` / `StoreConnectionFailureDescription` |
| `ProductType.INAPP` | `ProductType.Consumable` or `ProductType.NonConsumable` (distinguish by consume behavior) |
| `ProductType.INAPP` | `ProductType.Consumable` or `ProductType.NonConsumable`, decided by whether the native code consumes it |
| `ProductType.SUBS` | `ProductType.Subscription` |
| `offerToken` / `basePlanId` / `pricingPhases` | **Not directly exposed** — report as blocker |
| `offerToken` / `basePlanId` / `pricingPhases` | **Not exposed** — report as a blocker (Step 4) |

## Step 9 — Rollback Plan

Expand Down
Loading
Loading