A store purchase belongs to a store account; a SubKit entitlement belongs to an app user. When those don’t line up — shared devices, account switches, family sharing — SubKit reports the mismatch instead of silently transferring access.
Ownership on purchases
Each entry in customerInfo.purchases carries an ownership state:
ownership |
Meaning |
|---|---|
current |
Owned by the current app user |
alias |
Owned by a linked identity of the current user |
previous |
Owned by a user previously active on this install |
unowned |
Not attributed to any known user |
conflict |
Claimed by a different app user |
Plus canClaim (whether the current user could claim it) and conflict as a
quick flag.
Unclaimed purchases
customerInfo.unclaimedPurchases lists store purchases that exist but are not
attributed to the current user. Each carries a claimHint telling you what to
do:
claimHint |
UI action |
|---|---|
restore_required |
Offer the restore button (Restore & sync) |
login_required |
Ask the user to log in with the owning account |
support_required |
Route to support — automatic resolution is not safe |
const info = await client.getCustomerInfo()
for (const unclaimed of info.unclaimedPurchases) {
switch (unclaimed.claimHint) {
case 'restore_required':
showRestorePrompt()
break
case 'login_required':
showLoginPrompt()
break
case 'support_required':
showSupportLink()
break
}
}import { client } from '@piparotech/subkit-expo'Sync conflicts
A PurchaseSyncResult can contain conflicts when a synced purchase is
already bound to another app user. Each conflict carries a resolution
recommendation:
| Resolution | Meaning |
|---|---|
login_original_account |
The user should log in with the account that made the purchase |
manual_review |
An operator should review in the SubKit console |
support_required |
Needs support involvement |
The SDK does not transfer ownership. Conflicted purchases are not reconciled under the current user, and queued purchases stay bound to the app user that first observed them.
Beneficiary and device conflicts
Store restore first resolves a verified Store Purchase Lineage and its current Beneficiary. The installation ID is only a weak local input and never transfers ownership or creates a grant by itself.
The Effective Access union separates commercial access from installation
recovery. When the requested entitlement is active but this installation
cannot use it, useSubKitAccess(key) returns state: 'device_blocked' with a
typed reason. Raw customerInfo.deviceAccess remains available only for
advanced device-management diagnostics.
Handle device outcomes without telling the user to buy again:
DEVICE_SELECTION_REQUIRED: show the redacted device list and let the user choose an activation to replace.DEVICE_REPLACEMENT_COOLDOWN: shownextAllowedAt; do not blame the user or retry in a loop.DEVICE_CHANGE_LIMIT_REACHED: show the rollingchangeBudgetandnextAllowedAt.DEVICE_REPLACED: this installation was superseded. Cached offline authorization can remain valid only until the server-issued expiry.LOGIN_REQUIRED/BENEFICIARY_CONFLICT: require the documented account or support recovery path. Never silently transfer the Beneficiary.
Management-session tokens and Device Access tokens are distinct, short-lived, opaque capabilities. Do not parse, log, or expose them in UI.
Family sharing
Family-shared access appears as a regular entitlement with
source: 'family_shared'; the underlying purchase carries
ownershipType: 'family_shared'. Gate through useSubKitAccess(key) as usual —
no special handling is required unless your product excludes shared access.
Design guidance
- Treat conflicts as identity problems, not payment problems. The fix is logging in with the right account, not re-purchasing.
- Never show “buy again” as the primary action when an unclaimed purchase with
restore_requiredexists. - A valid purchase with blocked device access is not an inactive purchase. Keep commercial and installation messaging separate.