{"id":50167,"plugin_id":"plugin_asdk_app_6abf2061a1248191a70d61197a20542a","kind":"skill","collection_source":"plugin_package","comparison_source":null,"observed_at":"2026-10-24T13:42:16.820Z","digest":"5d5662f0cbe4f754dcf21464f1b66a83be46443686b81fbcb564d33fd39f5c24","against":null,"payload":{"description":"Implements Hive Axyl Google Play Billing in Unity on Android. Use for Google consumables, unconsumed-purchase recovery or subscription startup, including per-launch product/offer queries, purchase callbacks, receipt handoffs, post-then-consume and device acknowledgement. Google sign-in is a separate flow.","included_files":[{"relative_path":"references/new-purchase.md","size_in_bytes":2823},{"relative_path":"references/recovery.md","size_in_bytes":5451},{"relative_path":"references/subscription.md","size_in_bytes":4657}],"name":"axyl-purchase-market-google","skill_md_contents":"---\nname: axyl-purchase-market-google\ndescription: \"Implements Hive Axyl Google Play Billing in Unity on Android. Use for Google consumables, unconsumed-purchase recovery or subscription startup, including per-launch product/offer queries, purchase callbacks, receipt handoffs, post-then-consume and device acknowledgement. Google sign-in is a separate flow.\"\nlicense: Apache-2.0\nmetadata:\n  author: Com2uS Platform Corporation\n  sdk-version: 1.0.0\n  engine: Unity 6000.0+\n---\n\n# Google Play payments through market completion\n\nGoogle Play consumables, their recovery and subscription startup on Android. The\nInstalled SDK, SDK results, Payment state and Google Play Billing sections below\napply to every step.\n\n**Inputs:** configured products and offers, the originating player's session,\ncountry, language and the account UUID namespace.\n**App owns:** checkout UI, pending-state storage and the delivery or entitlement\nledger.\n\nRead only the requested goal:\n\n| Goal | Read | Completion |\n|---|---|---|\n| New consumable | [New purchase](references/new-purchase.md) | Pre → purchased event → record → game-server confirmation → post → consume |\n| Unfinished consumable | [Recovery](references/recovery.md) | In-app query → record again → game-server confirmation (recovery) → post → consume |\n| Subscription | [Subscription](references/subscription.md) | Subscription pre → store → save → game-server confirmation → subscription post → device acknowledgement |\n\nBefore enabling subscription checkout, read the\n[acknowledgement responsibility](references/subscription.md#google-acknowledgement-responsibility):\nAxyl's server acknowledges at post and the device acknowledges again, but nothing\nacknowledges before post, so plan deadline-aware recovery that survives app exit.\n\n## Verify\n\n- [ ] Each launch uses a fresh product query and its offer token; the handler is\n  subscribed before launch and only a correlated `Purchased` event continues.\n- [ ] The envelope goes to record and post, `PurchaseToken` to the game server's\n  consumable verification and to consume; subscriptions keep the envelope for save,\n  server verification and post; the client calls no verify API.\n- [ ] A failed or unknown post never consumes, `PurchaseNotFound` after a confirmed\n  post is closed, and subscriptions are acknowledged, never consumed.\n- [ ] Already owned leads to recovery; a failed purchase query is not zero purchases.\n- [ ] Device and server runs are reported separately; the reviewed sources contain none.\n\n<!-- axyl-shared: core/installed-sdk.md -->\n\n## Installed SDK\n\n- Read `Packages/manifest.json`, the lock file and each resolved `package.json`.\n  All `com.com2usplatform.hiveaxyl.*` packages must be the same release (UPM does\n  not enforce this); `com.hive.axyl.*` packages are a release before 1.0.0-rc.9.\n- The installed SDK wins for APIs, types and contracts. If its release differs\n  from this skill's `sdk-version`, apply a step only after its fields, result\n  variants, platform support and failure behaviour match; otherwise stop at that\n  step and report what is missing. Never change the installed version.\n- The app owns UI, storage, the device key and orchestration. Sample and recipe\n  classes are not SDK APIs.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: core/results.md -->\n\n## SDK results\n\nMatch each call's concrete result and read `Success.Data` only after checking the\nfields the next step needs. The markers live in `Hive.Axyl.Contracts.Result`.\n\n| Result | Handling |\n|---|---|\n| `IUserCanceledOutcome` | The user dismissed OS or provider UI: stop this attempt quietly |\n| `IUnknownOutcome` | Keep `Code` and `RawJson`; never treat it as a known refusal or a success |\n| `IUntypedProblem` | Technical failure in `Problem` (`HiveError`): network, timeout or caller `Cancelled` |\n| Other typed variant | Only that call's documented meaning |\n\nPass `new ApiCallContext { Token = ct }` and check cancellation before each\nfollow-up call; a request cancelled in flight may still have taken effect. Never\nbranch on HTTP status, exception text or raw bodies, and never log tokens or\ncredentials.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: core/payment-state.md -->\n\n## Payment state\n\nResolve `Hive.Axyl.Payments.IPaymentsService`, registered once at startup with\n`AddPayments()` (`https://commerce-api.hiveaxyl.com`), `AddPayments(sandbox: true)`\n(`https://sandbox-commerce-api.hiveaxyl.com`) or `AddPayments(baseUrl)` for a\nprovisioned host, as the app's environment selects. Require the originating\nplayer's authenticated session and the selected market's addon; never initialize\nper purchase. Never log receipts.\n\n### Retained state\n\n`PurchaseOrder`, `PendingPurchase`, `PurchaseDeliveryConfirmation` and the recipe\nsources are sample types: carry the same values in app-owned types through every\nstage.\n\n| State | Values |\n|---|---|\n| Order | Market, originating player, product ID, decimal price, currency, country, language, server ID, app version, IAP payload, account UUID; Steam store player ID |\n| Pending | Record receipt, verification token, finish token, order ID, store transaction ID, the order, any local store verification objection |\n| Confirmed | The game server's delivery confirmation for that pending purchase, with any updated closing receipt or verified subscription product ID |\n\nA missing addon or input, or a pending purchase from another market, stops before\nthe next mutation. Keep receipts and IDs after a failure or cancellation: payment\nor delivery may already have happened. Persist recoverable state under the app's\nstorage policy, and coordinate purchase, recovery and account changes so that no\ntransaction is delivered twice at once or moved to another player.\n\n`AccountUuid` is optional on `PrePurchase`, `PurchaseRequest` and\n`SubscriptionPrePurchaseRequest`; an unset one is left off, so the server cannot\ncompare the account. Set it: derive it as UUIDv5 from the game's fixed namespace UUID\n(from game configuration, the same on every platform) and the invariant decimal\nplayer ID string, in standard network byte order (mind C# `Guid`'s layout). Send the\nsame value as Apple `AppAccountToken`, Google `ObfuscatedAccountId` and in Axyl\nrequests; the SDK does not generate it.\n\n### Catalog\n\nApple/Google: use configured product IDs, or call\n`ListStoreProductIdsAsync(new StoreRequest { ProviderId, Country, Language,\nAppVersion }, ctx)` and select the intended entry of\n`PaymentsListStoreProductIdsResult.Success.Data.Stores`: `Products` for consumables,\n`ProductSubscriptions` for subscriptions (the request has no product type). An\nabsent or empty list calls for configured IDs or a catalog diagnosis; never reuse\nconsumable IDs as subscriptions or invent one. Then query the native store and send\nthe mapped details with the market's `Fetch*ProductsAsync`. Steam and PG fetch their\nserver catalog directly.\n\nEach fetch returns its own result over `ProductResponseData`. Choose the\n`Data.Products` entry by `ProductId` and `ProductType`, and keep its decimal `Price`,\n`Currency` and offers with the response's country and language as the purchase\ninputs. Never parse `DisplayPrice`, rescale a decimal into micros or turn a missing\nprice into zero (a configured zero-price phase is real). A catalog failure, an empty\ncatalog and a failed checkout are different results. Recovery keeps the original\ntransaction's price and uses the catalog only for missing data.\n\n### Receipt identities\n\n| Market | Record receipt | Verification token for the game server | Store finish |\n|---|---|---|---|\n| Apple | Original transaction `JwsRepresentation` | Same JWS | `AppleTransaction.Id` as decimal `ulong` |\n| Google | JSON envelope `{\"purchase_data\": OriginalJson, \"signature\": Signature}` | Consumable: `PurchaseToken` with `ProductId`; subscription: the envelope | Same `PurchaseToken` |\n| Steam | Server `AxylReceipt` from init or restore | Same sealed value | None on the device |\n| PG | Server `AxylReceipt` from restore | Same sealed value | None on the device |\n\nBuild the Google envelope with a JSON serializer around the untouched\n`OriginalJson` string; never parse and reserialize it. A payment URL is never a\nreceipt. Keep the order ID and store transaction ID as separate values: a restore\nentry's `StoreTransactionId` can differ from its order key.\n\nProvider enums are request-specific (`PrePurchaseProviderId`,\n`PurchaseRequestProviderId`, `PurchaseFinalizeRequestProviderId`, ...): use the named\n`Apple`, `Google`, `Steam` or `Pg` member where it exists; never cast between enums\nor default an unknown market.\n\n### Game-server verification, delivery, close\n\nReceipt verification is server to server: the Payments contract marks\n`/payment/v1/purchase/verify` and `/payment/v1/subscription/verify` server-only, so\nthe client SDK has no verify call. After the purchase is recorded, send the pending\npurchase to the game's own authenticated server: market, record receipt,\nverification token, order ID, store transaction ID, order metadata and account, and\nwhether it is a new purchase or a recovery (the server maps that to verify request\ntype 1 or 2).\n\nThe game server verifies with the payment backend, checks account, product and price\n(for a subscription also current refund and expiry eligibility) and grants once\nthrough its durable ledger. It answers with a delivery confirmation for that pending\npurchase, also when the same transaction was already granted, carrying any updated\nclosing receipt (for a subscription, the verified product ID). The endpoint, request\nand response are the app's design. The confirmation is an integration contract, not\nproof of payment: the client never manufactures one, and a rejection, failure,\ntimeout or missing confirmation keeps the pending purchase and closes nothing.\n\nClose only with a confirmation that matches the pending purchase, using the market's\nprocedure; `RequestPurchaseAsync` and `FinalizePurchaseAsync` run after delivery. A\nclose failure leaves a delivered, unfinished transaction: retry the close with the\nsame confirmation, never grant again. Remove the pending record only after the close\nsucceeds.\n\n`ItemResultAsync(ItemResultBody, ctx)` reports actual delivery (`AxylTransactionId`\nfrom the game server's verification; `Status` 1 success, 2 cancelled without\nclawback, 3 cancelled with clawback; `Assets` granted). The recipes do not use it. If\nthe integration requires it, agree its owner with the backend and report the ledger\nresult, never status 1 merely because a purchase was recorded.\n\n<!-- /axyl-shared -->\n\n<!-- axyl-shared: payments/google.md -->\n\n## Google Play Billing\n\nRegister `AddPlayBilling()` and resolve\n`Hive.Axyl.Payments.Addon.Google.IGooglePlayBillingPlugin` on Android, with the Play\nconfiguration and the selected catalog product. Connect first:\n`StartConnectionAsync(ct)` continues on `Success` or `AlreadyConnected`.\n\n### Catalog\n\nGet the product IDs as the catalog rules say and query them with\n`QueryProductDetailsAsync(ids, ProductType.Inapp or ProductType.Subs, ct)`. Native\nresults are `Hive.Axyl.Payments.Addon.Google.GoogleProductDetails`; server requests\ntake `Hive.Axyl.Payments.ProductDetails`. Check each product in\n`Success.Data.ProductDetailsList` and `UnfetchedProductList`: a partial success does\nnot mean the selected product was queried.\n\n- Map `ProductId`, `Title`, `Description` and the numeric `(int)ProductType`.\n- One-time offers (`OneTimePurchaseOfferDetails` and its list) map to\n  `ProductGoogleOfferDetails`: `FormattedPrice`, decimal `PriceAmount`,\n  `PriceCurrencyCode`, `OfferId`, `OfferToken`. Keep `PreorderDetails` and\n  `RentalDetails` as raw `ToJson()` strings when present; they affect price selection.\n- Subscription offers map to `ProductGoogleSubscriptionOffer` (`BasePlanId`,\n  `OfferId`, `OfferToken`, `OfferTags`, `PricingPhases`), and each phase to\n  `ProductGooglePricingPhase` (`FormattedPrice`, decimal `PriceAmount`,\n  `PriceCurrencyCode`, `BillingPeriod`, `BillingCycleCount`, numeric\n  `RecurrenceMode`). A trial phase is not the recurring phase.\n- The native price is already decimal: never multiply it or fill `PriceAmountMicros`.\n\nThen call `payments.FetchGoogleProductsAsync(new ProductGoogle { ProviderId =\nProductGoogleProviderId.Google, Country, Currency, Language, ProductType =\nrequestedType, AppVersion, Products = mappedProducts }, ctx)`, where the outer\n`requestedType` is `\"consumable\"` or `\"subscription\"`, and require\n`PaymentsFetchGoogleProductsResult.Success`.\n\n### Purchase\n\n1. After the pre-purchase succeeds, query the product again before **every**\n   launch with `QueryProductDetailsAsync(new[] { productId }, type, ct)` and require\n   it in `ProductDetailsList`; a launch without it fails with `PRODUCT_NOT_QUERIED`.\n   Take the `OfferToken` from this answer: the recipe uses the first subscription\n   offer with a token, else the first one-time offer with one, else none. Another\n   choice is the game's policy and must match the pre-purchase price.\n2. Subscribe to `PurchasesUpdated`, then call `LaunchBillingFlowAsync(new\n   LaunchBillingFlowRequest { Products = new[] { new LaunchBillingFlowProductParams {\n   ProductId = productId, OfferToken = selectedOfferToken } },\n   ObfuscatedAccountId = accountUuid }, ct)`.\n3. Launch success only opened the sheet. Wait for an event whose `Purchases` holds\n   this product in `PurchaseState.Purchased`, telling purchases apart by token, not\n   product. `BillingResult.ResponseCode` 0 means inspect the purchases, 1 user\n   cancellation and 7 already owned (recover that purchase, never check out again);\n   keep any other code as a technical failure. Pending is not purchased.\n4. Keep `OriginalJson`, `Signature`, `PurchaseToken`, `OrderId` and `Products`\n   untouched. Record and post send the envelope as `AxylReceipt`; the game server's\n   consumable verification uses the `PurchaseToken` with `ProductId`; `OrderId` is\n   both the order ID and `StoreTransactionId`.\n5. Unsubscribe on every exit. Caller cancellation stops the wait, not a Play\n   purchase in progress: keep any later transaction for app-owned reconciliation.\n\n### Close\n\n**Consumable:** after the game server's delivery confirmation, call\n`payments.RequestPurchaseAsync(new PurchasePostRequest { ProviderId =\nPurchasePostRequestProviderId.Google, AxylReceipt = recordedEnvelope, ProductId =\nproductId, StoreTransactionId = playOrderId }, ctx)`. Only `Success` or this call's\n`VerifyDuplicated` permits `ConsumeAsync(originalPurchaseToken, ct)`; never consume\nafter a failed or unknown post, because consume removes the purchase from Play's\nrecovery list. Axyl's server also consumes the purchase through the Play Developer\nAPI at post, so after a confirmed post, consume `PurchaseNotFound` means already\nclosed. The contract makes `productId` optional with the envelope and documents\n`storeTransactionId` for Apple only, but the recipe sends both and says the server\nneeds the product ID: send both, and treat that server behaviour as unconfirmed.\n\n**Subscription:** after save, the game server's confirmation and post, call\n`AcknowledgePurchaseAsync(originalPurchaseToken, ct)`; `AlreadyAcknowledged` is\nsuccess and anything else keeps it pending. Never consume a subscription. Axyl's\nserver has already acknowledged at post; this device call is the recipe's second,\nidempotent acknowledgement and always runs.\n\n**Recovery:** connect, then `QueryPurchasesAsync(ProductType.Inapp, ct)`; keep the\n`PurchaseState.Purchased` entries with products, map them like a fresh purchase (the\nentry has no price or currency), record again, hand it to the game server as a\nrecovery and close the same way.\n\nThe reviewed sources contain no device or live-server run of the Google paths, and\na multi-product purchase is recovered under its first product only.\n\n<!-- /axyl-shared -->\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}