BLE Hubs 1.0Think SignalR for BLE!Think SignalR for BLE!Hub it up!
UniHub 1.0Universal Hubs - SignalR Styled Hub BLE or HTTP - Why not both!?Universal Hubs - SignalR Styled Hub BLE or HTTP - Why not both!?YES PLEASE!
In-App Purchases
The in-app purchases bridge lets the page sell consumables, non-consumables and auto-renewable subscriptions through
the device’s own store: the App Store (StoreKit 2) on iOS and Google Play Billing on Android. It’s built on
Shiny.Mobile.InAppPurchases 5.9. The store’s sheet takes the payment, so the page never
sees card details. This is store purchasing, not Apple Pay or Google Pay.
AddInAppPurchasesBridge() registers Shiny’s purchase manager with the bridge’s delegate, so there’s nothing else to
call. An app that already calls services.AddInAppPurchases<MyPurchaseDelegate>() keeps its delegate, and Shiny runs
both. The products themselves are configured in App Store Connect and the Play Console. See
Store Setup.
Every route, its body and its response is listed under Requests.
Results, not errors: a purchase ends Success, Pending (Ask to Buy or a slow payment method), Cancelled
or, on Google Play, AlreadyOwned. Never grant a pending purchase. It arrives as purchases.updated when it
resolves.
Always send accountToken, your user’s id as a GUID. The stores echo it back as Apple’s appAccountToken and
Google’s obfuscatedAccountId, on the device and in their server notifications.
Offers:subscriptionOffers lists Google’s base plans and offers, and Apple’s introductory offer when the user
is eligible. Buy a Google offer by passing its offerToken. A Google upgrade or downgrade passes replacement
with the old product and its purchase token. Apple handles plan changes within a subscription group by itself.
A paid purchase is not finished by the bridge. Send verificationData (Apple’s signed JWS, Google’s purchase token) to
your server. Shiny.Mobile.InAppPurchases.Server’s IPurchaseVerifier takes it as
it is. Store the entitlement, and only then call POST purchases/transactions/{transactionId}/finish with
{ consume }. consume: true makes a consumable buyable again. Google Play refunds a purchase that isn’t finished
within three days.
The page only names the purchase. The bridge finds it among the store’s unfinished purchases and entitlements, so a
page can’t finish a purchase the store doesn’t have. It answers 404 purchase_not_found when there’s no match, which
usually means the purchase was finished already.
At startup, read GET purchases/unfinished and run each purchase through the same path, so a crash between paying and
finishing never loses one. GET purchases/entitlements is what the user owns now. POST purchases/restore syncs with
the App Store, which may ask the user to sign in, so call it only from a “Restore Purchases” button.
POST purchases/manage opens the store’s subscription screen.
Purchases change outside a purchase call: Ask to Buy approvals, pending payments that complete, renewals, refunds
(state: "Revoked"), purchases on another device and offer codes. Shiny listens from launch, and each change arrives
as purchases.updated. It runs in the page if one is listening, and otherwise in background.js, so a purchase that
completes with no page open is still verified and finished:
Google Play: one per base plan and per offer the user is eligible for; buy one with its offerToken. Apple: the introductory offer when the user is eligible, then promotional offers.
subscriptionGroupId
string | null
Apple's subscription group; null on Google Play and for one-time products.
Opens the store's purchase sheet. A cancelled sheet, a pending payment (Ask to Buy, a slow payment method) and a product already owned are results, not errors. A Success is not finished: verify and grant it, then call finish.
Your user's id. It comes back on every transaction and in the stores' server notifications (Apple appAccountToken, Google obfuscatedAccountId), so your server can tell whose purchase it is. Always set it.
offerToken
string | null
Google Play: the offerToken to buy; the first base plan when omitted. Ignored on Apple.
quantity
number
Apple: how many units of a consumable, 1 to 10. Google lets the user choose in the sheet when the product allows it.
Unique per charge — finish it by this. Apple: the transaction id, new on each renewal. Google: the order id, or the purchase token while the purchase is pending.
originalTransactionId
string
Stable across renewals. Apple: the original transaction id. Google: the purchase token.
Apple subscriptions only; Google does not report expiry on the device, so ask your server.
isFinished
boolean
Apple: finished. Google: acknowledged or consumed.
isAutoRenewing
boolean
A subscription set to renew.
quantity
number
Units bought.
accountToken
string | null
The accountToken it was bought with.
verificationData
string
Send this to your server to verify the purchase. Apple: the signed JWS transaction. Google: the purchase token. Shiny.Mobile.InAppPurchases.Server's IPurchaseVerifier takes it as it is.
Purchases that were paid for but never finished — check at startup, and run each through the same verify, grant, finish path, so a crash between payment and finishing never loses a purchase.
Unique per charge — finish it by this. Apple: the transaction id, new on each renewal. Google: the order id, or the purchase token while the purchase is pending.
originalTransactionId
string
Stable across renewals. Apple: the original transaction id. Google: the purchase token.
Apple subscriptions only; Google does not report expiry on the device, so ask your server.
isFinished
boolean
Apple: finished. Google: acknowledged or consumed.
isAutoRenewing
boolean
A subscription set to renew.
quantity
number
Units bought.
accountToken
string | null
The accountToken it was bought with.
verificationData
string
Send this to your server to verify the purchase. Apple: the signed JWS transaction. Google: the purchase token. Shiny.Mobile.InAppPurchases.Server's IPurchaseVerifier takes it as it is.
Finishes a purchase once its entitlement is stored. consume makes a consumable buyable again. The bridge finds the purchase in the store by its transaction id, and fails with 404 (purchase_not_found) when the store has no unfinished purchase or entitlement by it — typically because it was finished already.
True for a consumable, so it can be bought again; false acknowledges a non-consumable or subscription. Apple has no separate consume step, so this only matters on Google Play — but always say what the product is.
Restores purchases and returns the entitlements. On iOS this syncs with the App Store and may ask the user to sign in, so call it only from a "Restore Purchases" button.
Unique per charge — finish it by this. Apple: the transaction id, new on each renewal. Google: the order id, or the purchase token while the purchase is pending.
originalTransactionId
string
Stable across renewals. Apple: the original transaction id. Google: the purchase token.
Apple subscriptions only; Google does not report expiry on the device, so ask your server.
isFinished
boolean
Apple: finished. Google: acknowledged or consumed.
isAutoRenewing
boolean
A subscription set to renew.
quantity
number
Units bought.
accountToken
string | null
The accountToken it was bought with.
verificationData
string
Send this to your server to verify the purchase. Apple: the signed JWS transaction. Google: the purchase token. Shiny.Mobile.InAppPurchases.Server's IPurchaseVerifier takes it as it is.
A purchase changed outside a purchase call: an Ask to Buy approval, a pending payment that completed, a renewal, a refund, a purchase on another device, an offer code. The same purchase can arrive more than once, so handle it idempotently by transactionId. Also handed to a purchases.updated handler, so background.js can verify, grant and finish it when no page is open.
Unique per charge — finish it by this. Apple: the transaction id, new on each renewal. Google: the order id, or the purchase token while the purchase is pending.
originalTransactionId
string
Stable across renewals. Apple: the original transaction id. Google: the purchase token.
Apple subscriptions only; Google does not report expiry on the device, so ask your server.
isFinished
boolean
Apple: finished. Google: acknowledged or consumed.
isAutoRenewing
boolean
A subscription set to renew.
quantity
number
Units bought.
accountToken
string | null
The accountToken it was bought with.
verificationData
string
Send this to your server to verify the purchase. Apple: the signed JWS transaction. Google: the purchase token. Shiny.Mobile.InAppPurchases.Server's IPurchaseVerifier takes it as it is.
Google Play: one per base plan and per offer the user is eligible for; buy one with its offerToken. Apple: the introductory offer when the user is eligible, then promotional offers.
subscriptionGroupId
string | null
Apple's subscription group; null on Google Play and for one-time products.
Your user's id. It comes back on every transaction and in the stores' server notifications (Apple appAccountToken, Google obfuscatedAccountId), so your server can tell whose purchase it is. Always set it.
offerToken?
string | null
Google Play: the offerToken to buy; the first base plan when omitted. Ignored on Apple.
quantity?
number
Apple: how many units of a consumable, 1 to 10. Google lets the user choose in the sheet when the product allows it.
Unique per charge — finish it by this. Apple: the transaction id, new on each renewal. Google: the order id, or the purchase token while the purchase is pending.
originalTransactionId
string
Stable across renewals. Apple: the original transaction id. Google: the purchase token.
Apple subscriptions only; Google does not report expiry on the device, so ask your server.
isFinished
boolean
Apple: finished. Google: acknowledged or consumed.
isAutoRenewing
boolean
A subscription set to renew.
quantity
number
Units bought.
accountToken
string | null
The accountToken it was bought with.
verificationData
string
Send this to your server to verify the purchase. Apple: the signed JWS transaction. Google: the purchase token. Shiny.Mobile.InAppPurchases.Server's IPurchaseVerifier takes it as it is.
True for a consumable, so it can be bought again; false acknowledges a non-consumable or subscription. Apple has no separate consume step, so this only matters on Google Play — but always say what the product is.
The purchases routes sit under AppDeviceBridgePolicies.Bridges like every other bridge, so by default only callers
on this device can reach them. This bridge doesn’t change the default policy, the host-name check or the launch
session. Every purchase still needs the user’s confirmation in the store’s sheet. Verify verificationData on your
server before granting anything; never trust the device’s word for it. See Security.