{"_id":"@despia/user-identity","_rev":"7-2cd57ae70911bad13b59ab6f4a554c9e","name":"@despia/user-identity","dist-tags":{"latest":"1.0.5"},"versions":{"1.0.0":{"name":"@despia/user-identity","version":"1.0.0","keywords":["despia","identity","user","revenuecat","onesignal","app-user-id"],"author":{"name":"Despia"},"license":"MIT","_id":"@despia/user-identity@1.0.0","maintainers":[{"name":"despia","email":"developers@despia.com"}],"homepage":"https://github.com/despia-native/user-identity#readme","bugs":{"url":"https://github.com/despia-native/user-identity/issues"},"dist":{"shasum":"47bfa1ca4ef4042025d4c5c452e74bf0955ce955","tarball":"https://registry.npmjs.org/@despia/user-identity/-/user-identity-1.0.0.tgz","fileCount":7,"integrity":"sha512-Rvpc6AyUQg0qeUwZl9kpCdQaadqfUrvQ8UjbGmX0OE1PhsIIylbrLEHKAfs2ZcygJiMaHInX3dh3HVVZsipqWQ==","signatures":[{"sig":"MEUCICSiaRTWl6YoKAifNeFsEJVjBTo6sqELoUT/wHj3K52kAiEA1XjgWkunXBZNKf4CjKQwSXQcMl4hSwBc+/BeBA1ROgw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":35268},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"61f5aa39e71f7ab481d1c3b9835e70125b08491e","scripts":{"dev":"tsup src/index.ts --format cjs,esm --dts --watch","build":"tsup src/index.ts --format cjs,esm --dts --clean","prepublishOnly":"npm run build"},"_npmUser":{"name":"despia","email":"developers@despia.com"},"repository":{"url":"git+https://github.com/despia-native/user-identity.git","type":"git"},"_npmVersion":"11.6.2","description":"Resolves and persists app_user_id for Despia apps. You plug the value into RevenueCat, OneSignal, your backend, etc.","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","typescript":"^5.3.0","despia-native":"^1.0.0"},"peerDependencies":{"despia-native":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/user-identity_1.0.0_1771913686412_0.058229380127835295","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."},"1.0.1":{"name":"@despia/user-identity","version":"1.0.1","keywords":["despia","identity","user","revenuecat","onesignal","app-user-id"],"author":{"name":"Despia"},"license":"MIT","_id":"@despia/user-identity@1.0.1","maintainers":[{"name":"despia","email":"developers@despia.com"}],"homepage":"https://github.com/despia-native/user-identity#readme","bugs":{"url":"https://github.com/despia-native/user-identity/issues"},"dist":{"shasum":"2a995f6ad07f23cc59ff167c7ff2236ecac2f145","tarball":"https://registry.npmjs.org/@despia/user-identity/-/user-identity-1.0.1.tgz","fileCount":7,"integrity":"sha512-OSLS+HRP1MXB+zgVYtphNKvx8yc9JBBWBX09Nlc9Y9pZAyvQsKNG4k7+OlPN0qqg56vf2+Pqvt8czY2/YZ7syQ==","signatures":[{"sig":"MEYCIQD+11ooCZmh2doXgvcGm6k1gu69SHI2UM9UHJAeMsv3ywIhAMA7ApZeFvTKsqt/pxZxburMtxPFpbr9e+MGiezN1bee","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":38007},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"5c525642438f58a2332802a15b1e381703d88938","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"despia","email":"developers@despia.com"},"repository":{"url":"git+https://github.com/despia-native/user-identity.git","type":"git"},"_npmVersion":"11.6.2","description":"Resolves and persists app_user_id for Despia apps. You plug the value into RevenueCat, OneSignal, your backend, etc.","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"8.0.0","typescript":"5.3.0","despia-native":"1.0.0"},"peerDependencies":{"despia-native":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/user-identity_1.0.1_1771921907613_0.8013385859685407","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."},"1.0.2":{"name":"@despia/user-identity","version":"1.0.2","keywords":["despia","identity","user","revenuecat","onesignal","app-user-id"],"author":{"name":"Despia"},"license":"MIT","_id":"@despia/user-identity@1.0.2","maintainers":[{"name":"despia","email":"developers@despia.com"}],"homepage":"https://github.com/despia-native/user-identity#readme","bugs":{"url":"https://github.com/despia-native/user-identity/issues"},"dist":{"shasum":"98f1bd3d60ddf665da50bb55f64bc1a641c4adfa","tarball":"https://registry.npmjs.org/@despia/user-identity/-/user-identity-1.0.2.tgz","fileCount":7,"integrity":"sha512-42MK9k5kNioPums9sYEt8Q8JqKWJiRW4WPe6FFh/fcaf6fYS/t1eT+Hmug5EPjprz6QtjJAgNMtQPYEg01zjnw==","signatures":[{"sig":"MEUCIBAJWkqk1FwwJM/sH/+yggN1peaeFNinTTZzt1I2Z1vvAiEApYZ8cWYDM1d9exuVU2oo/S9U657bqdB4PLJfwhctoPY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":42696},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"480be30cab22674b2a92a45f8e10571e218fd012","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"despia","email":"developers@despia.com"},"repository":{"url":"git+https://github.com/despia-native/user-identity.git","type":"git"},"_npmVersion":"11.6.2","description":"Resolves and persists app_user_id for Despia apps. You plug the value into RevenueCat, OneSignal, your backend, etc.","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"8.0.0","typescript":"5.3.0","despia-native":"1.0.0"},"peerDependencies":{"despia-native":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/user-identity_1.0.2_1771924466269_0.4798981235558175","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."},"1.0.3":{"name":"@despia/user-identity","version":"1.0.3","keywords":["despia","identity","user","revenuecat","onesignal","app-user-id"],"author":{"name":"Despia"},"license":"MIT","_id":"@despia/user-identity@1.0.3","maintainers":[{"name":"despia","email":"developers@despia.com"}],"homepage":"https://github.com/despia-native/user-identity#readme","bugs":{"url":"https://github.com/despia-native/user-identity/issues"},"dist":{"shasum":"7713031748118ef96b139bd4d260aaaca97f614b","tarball":"https://registry.npmjs.org/@despia/user-identity/-/user-identity-1.0.3.tgz","fileCount":7,"integrity":"sha512-PHiqFheeM7xH0z3diZlrxmvgEIyRq/+PNGyyWUhSS7kvpR933UK6RydU10tQoZ5Bm12RZmBspLt6jOVg4+FPnw==","signatures":[{"sig":"MEYCIQDVuRu5aMeWtS0AANjEV3dZWSJ35H8vkMN8fjbnNMdc6wIhAPk80m3tFd085BVX1QY1KySVSp5BfUbenW2PQP8bBlJD","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":46716},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"6d2492a0af275513b9a3f0846e3cd98eefcb46c5","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"despia","email":"developers@despia.com"},"repository":{"url":"git+https://github.com/despia-native/user-identity.git","type":"git"},"_npmVersion":"11.6.2","description":"Resolves and persists app_user_id for Despia apps. You plug the value into RevenueCat, OneSignal, your backend, etc.","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"8.0.0","typescript":"5.3.0","despia-native":"1.0.0"},"peerDependencies":{"despia-native":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/user-identity_1.0.3_1771932994393_0.3381477562389148","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."},"1.0.4":{"name":"@despia/user-identity","version":"1.0.4","keywords":["despia","identity","user","revenuecat","onesignal","app-user-id"],"author":{"name":"Despia"},"license":"MIT","_id":"@despia/user-identity@1.0.4","maintainers":[{"name":"despia","email":"developers@despia.com"}],"homepage":"https://github.com/despia-native/user-identity#readme","bugs":{"url":"https://github.com/despia-native/user-identity/issues"},"dist":{"shasum":"2ffa4803bf6aaf1b45616d5d1b9f72af377bdbc6","tarball":"https://registry.npmjs.org/@despia/user-identity/-/user-identity-1.0.4.tgz","fileCount":7,"integrity":"sha512-evV86U2HevgGr+MCBayPlNPyf6Fvd7etGHZ7U9wK5ijC+wbMOopotPmhNqaTh6U4RhM9eGzY8/YsRb1ZCpEGog==","signatures":[{"sig":"MEYCIQDaQzgtuGAqMT4eV4l86rpObUfZGLagT5zhAacLZunupAIhAJoobVwbWt0oqd00joJ47EmP+KFve266iTI2WxG7mREU","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":45259},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"df29079fc64ac0b34c5b522964508c36aa4335dd","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"despia","email":"developers@despia.com"},"repository":{"url":"git+https://github.com/despia-native/user-identity.git","type":"git"},"_npmVersion":"11.6.2","description":"Resolves and persists app_user_id for Despia apps. You plug the value into RevenueCat, OneSignal, your backend, etc.","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"8.0.0","typescript":"5.3.0","despia-native":"1.0.0"},"peerDependencies":{"despia-native":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/user-identity_1.0.4_1771934669264_0.8758678290572157","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."},"1.0.5":{"name":"@despia/user-identity","version":"1.0.5","keywords":["despia","identity","user","revenuecat","onesignal","app-user-id"],"author":{"name":"Despia"},"license":"MIT","_id":"@despia/user-identity@1.0.5","maintainers":[{"name":"despia","email":"developers@despia.com"}],"homepage":"https://github.com/despia-native/user-identity#readme","bugs":{"url":"https://github.com/despia-native/user-identity/issues"},"dist":{"shasum":"697198d24ae2b24215a44b47d48dc7749bdeef13","tarball":"https://registry.npmjs.org/@despia/user-identity/-/user-identity-1.0.5.tgz","fileCount":7,"integrity":"sha512-7dCM3q2tlWc2vCFI2cKMqrBoPBzt2wnhvsg0V3G2bgM2cJ5ul1odXcImAS/wvLJvTANE5heHE38ZZkpfvFcMgQ==","signatures":[{"sig":"MEUCIDvxoRHNyTwo0uWL2UD8fQJGthZ9XFsl8T+ttNmnN8TbAiEAvxbYin4UK5BtcqQDaeEwB/P0RPvkeFHCofPdEr7cbMc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":45259},"main":"dist/index.js","types":"dist/index.d.ts","module":"dist/index.mjs","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"gitHead":"df29079fc64ac0b34c5b522964508c36aa4335dd","scripts":{"dev":"tsup --watch","build":"tsup","prepublishOnly":"npm run build"},"_npmUser":{"name":"despia","email":"developers@despia.com"},"repository":{"url":"git+https://github.com/despia-native/user-identity.git","type":"git"},"_npmVersion":"11.6.2","description":"Resolves and persists app_user_id for Despia apps. You plug the value into RevenueCat, OneSignal, your backend, etc.","directories":{},"_nodeVersion":"24.12.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"8.0.0","typescript":"5.3.0","despia-native":"1.0.0"},"peerDependencies":{"despia-native":">=1.0.0"},"_npmOperationalInternal":{"tmp":"tmp/user-identity_1.0.5_1771934725048_0.3957418338683585","host":"s3://npm-registry-packages-npm-production"},"deprecated":"Package no longer supported. Contact Support at https://www.npmjs.com/support for more info."}},"time":{"created":"2026-02-24T06:14:46.273Z","modified":"2026-08-29T10:23:03.486Z","1.0.0":"2026-02-24T06:14:46.555Z","1.0.1":"2026-02-24T08:31:47.755Z","1.0.2":"2026-02-24T09:14:26.391Z","1.0.3":"2026-02-24T11:36:34.536Z","1.0.4":"2026-02-24T12:04:29.407Z","1.0.5":"2026-02-24T12:05:25.198Z"},"bugs":{"url":"https://github.com/despia-native/user-identity/issues"},"author":{"name":"Despia"},"license":"MIT","homepage":"https://github.com/despia-native/user-identity#readme","keywords":["despia","identity","user","revenuecat","onesignal","app-user-id"],"repository":{"url":"git+https://github.com/despia-native/user-identity.git","type":"git"},"description":"Resolves and persists app_user_id for Despia apps. You plug the value into RevenueCat, OneSignal, your backend, etc.","maintainers":[{"name":"despia","email":"developers@despia.com"}],"readme":"# @despia/user-identity\n\n[Repository](https://github.com/despia-native/user-identity)\n\n---\n\n## What this package is\n\n**@despia/user-identity** is the standard way to get and persist a single, stable user ID (`app_user_id`) in Despia native apps (iOS and Android). It does **only** the following:\n\n1. **Resolves** who the user is: **vault first** (device’s stored identity — iCloud/Android backup). If nothing in the vault, then restore from purchase history (RevenueCat), then fall back to a new install ID.\n2. **Persists** that ID in the device vault so it survives app restarts and reinstalls (when backup is used).\n3. **Returns** that ID to you so you can pass it to RevenueCat, OneSignal, your backend, and any other service.\n\nThis package **does not** register users with your backend, sync to OneSignal, or call RevenueCat. It only gives you the `app_user_id`; you send it to your backend and other services yourself. It **does not** require users to sign in; it works without accounts or passwords.\n\n---\n\n## What are user sessions in Despia?\n\nEvery Despia app needs one consistent user identifier (`app_user_id`) that:\n\n- Stays the same across app restarts, reinstalls, and device changes\n- Links all your services together (purchases, push notifications, backend, etc.)\n- Works **without requiring users to sign in** - no password, no account, no login screen\n\nThis package gives you that identifier. It’s the standard way Despia apps manage user identity.\n\n---\n\n## Why this matters\n\n**Without a stable identity, users get “lost” when they reinstall.** They lose subscriptions, trials, credits, or preferences. You can’t recognize them or enforce your rules.\n\n**This package solves that** by using:\n\n1. **iCloud Key-Value store** (iOS) and **Android backup** - identity survives reinstall when the user backs up\n2. **Restore purchases** - if they bought something before, identity is recovered from purchase history\n3. **Install ID** - fallback for brand-new users\n\nYou get a stable `app_user_id` without forcing users to create an account. They keep access to what they paid for.\n\n**It also helps prevent fraud.** The same identity lets you detect when someone uninstalls and creates a new account to claim another free trial or bonus credits. You can enforce limits and keep paying users correctly linked.\n\n---\n\n## Install\n\nInstall the package and the Despia native SDK (required):\n\n```bash\n# npm\nnpm install @despia/user-identity despia-native\n\n# pnpm\npnpm add @despia/user-identity despia-native\n\n# yarn\nyarn add @despia/user-identity despia-native\n```\n\nYou must have `despia-native` in your app; this package uses it and does not bundle it (no duplicate React or Despia instance).\n\n---\n\n## Detailed step-by-step instructions\n\nFollow these steps in order. Do not show paywalls, push registration, or any user-specific UI until identity is ready.\n\n### Step 1: Resolve identity at app entry\n\n**Where:** The first place your app runs (e.g. root layout, `App.tsx`, or main entry before any route that needs the user).\n\n**What to do:**\n\n1. Import the package and `despia-native`:\n   ```ts\n   import despia from 'despia-native';\n   import { userIdentity, isDespia, getPlatform } from '@despia/user-identity';\n   ```\n\n2. Call `userIdentity()` once and await it. This reads the vault and/or purchase history and returns the user ID (or creates a new one).\n   ```ts\n   const result = await userIdentity();\n   ```\n\n3. Check the result:\n   - If `result === null`: the app is running on **web** (not in Despia). Use your own web identity (session, cookies, etc.) and skip the steps below for native.\n   - If `result` is an object: you are in the **Despia native app**. Continue.\n\n4. From `result` you get: `appUserId`, `installId`, `source`, `aliases`. Use `appUserId` everywhere you need a user identifier (RevenueCat, OneSignal, backend). The `source` tells you where the ID came from: `'vault'` (restored from backup), `'restore'` (from purchase history), or `'new'` (first install).\n\n### Step 2: Send the ID to your backend (do not block the UI)\n\n**Important:** Do **not** `await` the backend call. If you await it, a slow or cold-starting backend can block your app for minutes. Fire the request and forget.\n\n1. Call your register endpoint with the identity payload. Example:\n   ```ts\n   if (result) {\n     const { appUserId, installId, source } = result;\n     const platform = getPlatform(); // 'ios' | 'android' | null\n     fetch('https://your-api.com/api/user/register', {\n       method: 'POST',\n       headers: { 'Content-Type': 'application/json' },\n       body: JSON.stringify({\n         appUserId,\n         deviceId: installId,\n         source,\n         platform: platform ?? 'unknown',\n         timestamp: new Date().toISOString(),\n       }),\n     }).catch(() => {}); // fire-and-forget; do not await\n   }\n   ```\n\n2. Your backend should upsert a user by `app_user_id` and link the device. The user can use the app immediately; the backend just needs to receive the data eventually.\n\n### Step 3: Sync the ID to RevenueCat and OneSignal\n\nUse the same `appUserId` so purchases and push are tied to one user.\n\n1. **RevenueCat:** Whenever you open a paywall or set the user in RevenueCat, pass `appUserId` as the `external_id`. Example:\n   ```ts\n   despia(`revenuecat://launchPaywall?external_id=${appUserId}&offering=default`);\n   ```\n   If you do not pass this `external_id`, restore-from-purchases and identity recovery will not work correctly.\n\n2. **OneSignal:** Set the external user ID so push is linked to this user. Example (also fire-and-forget; do not block):\n   ```ts\n   despia(`setonesignalplayerid://?user_id=${appUserId}`);\n   ```\n\n### Step 4: Add a \"Restore purchases\" button\n\nApp Store and Play Store require a way to restore purchases. Add a button (e.g. on Settings or the paywall) that:\n\n1. Calls `restore()` from this package:\n   ```ts\n   import { restore } from '@despia/user-identity';\n   const restored = await restore();\n   ```\n\n2. If `restored` is not null, you get `restored.appUserId`. Sync it to OneSignal and your backend (again, fire-and-forget). If `restored` is null, no purchase with an `externalUserId` was found; show a message like \"No purchases found.\"\n\n### Step 5: If you have login, wire it to identity\n\nWhen the user signs in, your backend returns the canonical `app_user_id` (CLAIM or RECOVER). Persist it with this package so future launches use it:\n\n1. After a successful login response that includes `appUserId`:\n   ```ts\n   import { setAppUserId } from '@despia/user-identity';\n   await setAppUserId(appUserIdFromBackend);\n   ```\n\n2. Then sync that ID to OneSignal and your backend as in Steps 2 and 3.\n\n---\n\n## Usage (full example)\n\n```ts\nimport despia from 'despia-native';\nimport { userIdentity, restore, isDespia, getPlatform } from '@despia/user-identity';\n\n// At app entry - before any paywall, push, or user-specific feature\nconst result = await userIdentity();\n\nif (result) {\n  const { appUserId, installId, source, aliases } = result;\n\n  // Sync to services (fire-and-forget; do not await)\n  despia(`revenuecat://launchPaywall?external_id=${appUserId}&offering=default`);\n  despia(`setonesignalplayerid://?user_id=${appUserId}`);\n  fetch('/api/user/register', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({\n      appUserId,\n      deviceId: installId,\n      source,\n      platform: getPlatform() ?? 'unknown',\n      timestamp: new Date().toISOString(),\n    }),\n  }).catch(() => {});\n} else {\n  // Web: use your own identity (session, etc.)\n}\n\n// Restore purchases button (e.g. in Settings)\nasync function onRestorePurchases() {\n  const restored = await restore();\n  if (restored) {\n    const { appUserId } = restored;\n    despia(`setonesignalplayerid://?user_id=${appUserId}`);\n    fetch('/api/user/register', {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ appUserId, source: 'restore_button', timestamp: new Date().toISOString() }),\n    }).catch(() => {});\n  }\n}\n```\n\n**Do not block app load on backend or OneSignal.** Call `userIdentity()` and then fire registration and OneSignal in the background without awaiting them.\n\n---\n\n## When each source is used\n\n| Source   | When it happens |\n|----------|------------------|\n| `vault`  | User had this device before and iCloud/Android backup restored the stored ID |\n| `restore`| Vault was empty but purchase history had an `externalUserId` (from RevenueCat) - we recovered it |\n| `new`    | First install, no backup, no purchases - we use the device install ID |\n\n---\n\n## User ID flows (enterprise reference)\n\nThis section documents all identity flows end-to-end for integration, audits, and support.\n\n### Flow quick reference\n\n| Flow | Trigger | Source | Outcome |\n|------|---------|--------|---------|\n| 1 | Every app launch | - | Resolution: vault → restore → new |\n| 2 | First install | `new` | Install ID used, persisted to vault |\n| 3 | Reinstall + backup restored | `vault` | Same ID from iCloud/Android |\n| 4 | Reinstall, no backup | `restore` | Recovered from purchase history |\n| 5 | New device | `restore` or `new` | Purchase sync or install ID |\n| 6 | Login, account new | CLAIM | Device ID linked to account |\n| 7 | Login, account exists | RECOVER | Device switches to account ID |\n| 8 | Restore purchases button | - | Recovered from store, vault updated |\n| 9 | Backend merge | - | `linkAlias()` for fraud/audit |\n\n---\n\n### Flow 1: Identity resolution (every app launch)\n\n**Vault first (highest priority).** We only restore from purchases or use install ID when the vault is empty. `userIdentity()` runs this logic in order:\n\n```mermaid\nflowchart TD\n    A[Read Storage Vault<br/>iCloud / Android backup] --> B{Found?}\n    B -->|Yes| C[Use it. source = vault]\n    B -->|No| D[Query purchase history<br/>RevenueCat / App Store / Play Store]\n    D --> E{Any purchase with<br/>externalUserId?}\n    E -->|Yes| F[Use it. source = restore<br/>Lock identity to vault]\n    E -->|No| G[Fallback to install ID<br/>source = new]\n    C --> H[Persist to vault]\n    F --> H\n    G --> H\n    H --> I[Return appUserId, installId, source, aliases]\n```\n\n**Critical**: Always pass `appUserId` as `external_id` when launching RevenueCat paywalls. Otherwise step 2 will never find a recoverable ID.\n\n---\n\n### Flow 2: First-time user (no backup, no purchases)\n\n| Step | Client | Backend | Vault |\n|------|--------|---------|-------|\n| 1 | App launches, vault empty | - | - |\n| 2 | `userIdentity()` → vault empty, no purchases | - | - |\n| 3 | Uses install ID, `source: 'new'` | - | - |\n| 4 | Persists install ID to vault | - | `app_user_id` = install ID |\n| 5 | POST /api/user/register | Upsert user, link device | - |\n| 6 | Sync to RevenueCat, OneSignal | - | - |\n\n**Result**: `appUserId` = install ID. Same user on same device will keep this ID across restarts.\n\n---\n\n### Flow 3: Returning user - reinstall with iCloud/Android backup\n\n| Step | Client | Backend | Vault |\n|------|--------|---------|-------|\n| 1 | User reinstalls app | - | - |\n| 2 | iCloud/Android restores vault before app runs | - | `app_user_id` = previous ID |\n| 3 | `userIdentity()` reads vault | - | - |\n| 4 | Returns `source: 'vault'` | - | - |\n| 5 | POST /api/user/register | Recognizes existing user | - |\n\n**Result**: Same `appUserId` as before reinstall. Subscriptions, credits, and preferences preserved.\n\n---\n\n### Flow 4: Returning user - reinstall without backup (restore from purchases)\n\n| Step | Client | Backend | Vault |\n|------|--------|---------|-------|\n| 1 | User reinstalls, backup not yet restored | - | Empty |\n| 2 | `userIdentity()` → vault empty | - | - |\n| 3 | Queries purchase history | - | - |\n| 4 | Finds purchase with `externalUserId` (from previous paywall launch) | - | - |\n| 5 | Uses that ID, `source: 'restore'`, locks to vault | - | `app_user_id` = recovered ID |\n| 6 | POST /api/user/register | Recognizes existing user | - |\n\n**Result**: Same `appUserId` as when they originally purchased. Requires purchases were made with `external_id` = `app_user_id`.\n\n---\n\n### Flow 5: New device, same Apple/Google account\n\n| Step | Client | Backend | Vault |\n|------|--------|---------|-------|\n| 1 | User gets new phone, installs app | - | Empty (new device) |\n| 2 | Purchase history may sync from App Store / Play Store | - | - |\n| 3 | If purchase history has `externalUserId` → `source: 'restore'` | - | Recovered ID written |\n| 4 | If not yet synced → `source: 'new'`, install ID used | - | Install ID written |\n| 5 | User taps \"Restore purchases\" later | - | - |\n| 6 | `restore()` finds purchase with `externalUserId` | - | Vault updated |\n| 7 | Sync to backend, OneSignal, RevenueCat | - | - |\n\n**Result**: Identity recovered when store syncs purchase history, or when user restores purchases.\n\n---\n\n### Flow 6: Login - CLAIM (account has no app_user_id)\n\nUser signs in for the first time. Their account has no linked `app_user_id`.\n\n| Step | Client | Backend | Vault |\n|------|--------|---------|-------|\n| 1 | `userIdentity()` returns `appUserId` (e.g. install ID) | - | - |\n| 2 | User logs in | - | - |\n| 3 | POST /api/user/login { accountId, currentAppUserId } | - | - |\n| 4 | Backend: account has no app_user_id | - | - |\n| 5 | Backend: **CLAIM** - save currentAppUserId to account | - | - |\n| 6 | Returns { appUserId: currentAppUserId, action: 'claimed' } | - | - |\n| 7 | Client calls `setAppUserId(appUserId)` | - | Same ID (already in vault) |\n| 8 | Sync to RevenueCat, OneSignal | - | - |\n\n**Result**: Anonymous device identity is now linked to the account. Future logins from this device use this ID.\n\n---\n\n### Flow 7: Login - RECOVER (account already has app_user_id)\n\nUser signs in on a new device. Their account already has an `app_user_id` from another device.\n\n| Step | Client | Backend | Vault |\n|------|--------|---------|-------|\n| 1 | New device: `userIdentity()` returns `appUserId` = install ID | - | - |\n| 2 | User logs in | - | - |\n| 3 | POST /api/user/login { accountId, currentAppUserId } | - | - |\n| 4 | Backend: account has app_user_id = \"user-abc\" | - | - |\n| 5 | Backend: **RECOVER** - return existing ID | - | - |\n| 6 | Returns { appUserId: \"user-abc\", action: 'recovered' } | - | - |\n| 7 | Client calls `setAppUserId(\"user-abc\")` | - | Vault overwritten with \"user-abc\" |\n| 8 | Sync to RevenueCat, OneSignal | - | - |\n\n**Result**: Device now uses the account’s canonical ID. Purchases and entitlements from other devices apply here.\n\n---\n\n### Flow 8: Restore purchases button\n\nRequired by App Store and Play Store. User taps \"Restore purchases\" (e.g. in Settings).\n\n| Step | Client | Backend | Vault |\n|------|--------|---------|-------|\n| 1 | User taps \"Restore purchases\" | - | - |\n| 2 | `restore()` queries purchase history | - | - |\n| 3 | Finds purchase with `externalUserId` | - | - |\n| 4 | Writes ID to vault, locks identity | - | `app_user_id` updated |\n| 5 | Returns { appUserId, aliases } | - | - |\n| 6 | Client syncs appUserId to RevenueCat, OneSignal, POST /api/user/register | - | - |\n\n**Result**: Identity recovered from store. Use `source: 'restore_button'` when registering so backend can attribute the source.\n\n---\n\n### Flow 9: Aliases and fraud detection\n\nWhen backend detects the same person under multiple IDs (e.g. same device, same receipt):\n\n| Step | Client | Backend | Vault |\n|------|--------|---------|-------|\n| 1 | Backend merges IDs, returns instruction to link | - | - |\n| 2 | Client calls `linkAlias(oldInstallId)` | - | Aliases array updated |\n| 3 | Next `userIdentity()` returns { appUserId, aliases: [oldInstallId] } | - | - |\n| 4 | Send aliases to backend for fraud checks | - | - |\n\n**Result**: Main ID stays canonical. Aliases support auditing and fraud prevention.\n\n---\n\n### Backend decision logic: CLAIM vs RECOVER\n\n```mermaid\nflowchart TD\n    A[POST /api/user/login] --> B[Look up account by accountId]\n    B --> C{Does account have<br/>app_user_id?}\n    C -->|No| D[CLAIM]\n    D --> E[Save currentAppUserId to account]\n    E --> F[Return appUserId: currentAppUserId<br/>action: claimed]\n    C -->|Yes| G[RECOVER]\n    G --> H[Return appUserId: account.app_user_id<br/>action: recovered]\n```\n\n---\n\n### Service sync order (recommended)\n\nAfter `userIdentity()` or `restore()` or `setAppUserId()`:\n\n1. **RevenueCat** - `despia('revenuecat://launchPaywall?external_id=' + appUserId)` (or set external ID via SDK)\n2. **OneSignal** - `despia('setonesignalplayerid://?user_id=' + appUserId)`\n3. **Backend** - `POST /api/user/register` with `appUserId`, `deviceId`, `source`, `platform`, `timestamp`\n\n---\n\n## Backend integration\n\nYour backend stores `app_user_id` as the main user identifier. It receives it in two flows:\n\n### 1. Register (on every app launch)\n\nOn each launch, the app sends the resolved `app_user_id` to your backend so you can track devices and know who’s active:\n\n```ts\n// Client (after userIdentity())\nawait fetch('/api/user/register', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    appUserId,\n    deviceId: installId,\n    source,        // 'vault' | 'restore' | 'new'\n    platform: 'ios' | 'android',\n    timestamp: new Date().toISOString()\n  })\n});\n```\n\nYour backend upserts a user and links the device. Use this for analytics, last-seen, and knowing which devices belong to whom.\n\n### 2. Login (CLAIM vs RECOVER)\n\nWhen a user signs in, the backend must decide whether to **CLAIM** or **RECOVER**:\n\n| Action | When | What the backend does |\n|--------|------|------------------------|\n| **CLAIM** | Account has no `app_user_id` yet | Link the current `app_user_id` from the device to this account |\n| **RECOVER** | Account already has an `app_user_id` | Return that existing ID so the device switches to it |\n\nThe client sends `currentAppUserId` (from `userIdentity()`). The backend replies with the canonical `app_user_id` to use. The client then calls `setAppUserId(returnedId)` so the vault and future resolution use it:\n\n```ts\n// Client\nconst { appUserId, action } = await fetch('/api/user/login', {\n  method: 'POST',\n  body: JSON.stringify({\n    accountId: 'user-123',\n    currentAppUserId: result.appUserId,\n    credentials: { email, password }\n  })\n}).then(r => r.json());\n\nawait setAppUserId(appUserId);\n// action is 'claimed' or 'recovered'\n```\n\nThe backend should store `app_user_id` as the primary key or foreign key for that user. RevenueCat, OneSignal, webhooks, and any other service should map back to this ID.\n\n### Minimal database schema\n\n```sql\n-- Users: app_user_id is the main identifier\nCREATE TABLE users (\n  app_user_id TEXT PRIMARY KEY,\n  created_at TIMESTAMPTZ DEFAULT NOW(),\n  last_seen TIMESTAMPTZ DEFAULT NOW()\n);\n\n-- Devices: link device_id to app_user_id\nCREATE TABLE user_devices (\n  device_id TEXT PRIMARY KEY,\n  app_user_id TEXT REFERENCES users(app_user_id),\n  platform TEXT,  -- 'ios' | 'android'\n  last_seen TIMESTAMPTZ DEFAULT NOW()\n);\n\n-- Accounts (if you have login): link account to app_user_id\nCREATE TABLE accounts (\n  id UUID PRIMARY KEY,\n  app_user_id TEXT REFERENCES users(app_user_id),\n  email TEXT,\n  ...\n);\n```\n\nRegister endpoint: upsert into `users` and `user_devices` on each POST. Login endpoint: look up account, return `app_user_id` (CLAIM or RECOVER).\n\n### 3. RevenueCat webhook (grant access on purchase)\n\nThis package does not call RevenueCat; it only gives you `app_user_id`. To grant access when a user pays, use a **RevenueCat webhook** on your backend.\n\n1. In RevenueCat, set the webhook URL to your backend (e.g. `https://your-api.com/webhooks/revenuecat`).\n2. RevenueCat sends events (e.g. `INITIAL_PURCHASE`, `RENEWAL`) with `app_user_id` (RevenueCat calls it `app_user_id` or the external ID you set).\n3. Your backend finds the user by `app_user_id` and updates subscription status (e.g. set `subscription_status = 'active'`).\n4. The app can then poll your backend or use your own logic to show premium content. Do not grant access only from the client; the webhook is the source of truth.\n\nRelevant for this package: the webhook payload’s user identifier is the same `app_user_id` you get from `userIdentity()` and pass as `external_id` to RevenueCat. Use it to match events to your `users` table. See [RevenueCat webhooks](https://www.revenuecat.com/docs/webhooks) for payload shape and setup.\n\n---\n\n## Main ID + aliases\n\nYou get a single main `appUserId` and an optional list of `aliases` (other IDs linked to the same user). Use the main ID for RevenueCat, OneSignal, backend, etc. Use aliases for fraud checks or when your backend merges users:\n\n```ts\nconst result = await userIdentity();\n// result.appUserId = main/canonical ID\n// result.aliases = ['old_install_id', 'merged_user_id']\n\n// When backend says \"merge ID X into this user\":\nawait linkAlias('old_install_id');\n```\n\n---\n\n## Web vs native\n\n- **Native Despia app** (iOS/Android): `await userIdentity()` returns `{ appUserId, installId, source, aliases }`. Use it.\n- **Web (browser)**: `await userIdentity()` returns `null`. The package does nothing. Use your own web identity (session, cookies, etc.).\n- **Check before use**: Always check `if (result)` or `if (isDespia())` before syncing to RevenueCat, OneSignal, or backend.\n\n**Platform detection:** Despia sets the user agent so you can detect native runtime and platform. Official guide: [User Agent](https://setup.despia.com/native-features/user-agent) (no package required; use `navigator.userAgent.toLowerCase().includes('despia')` and check for `iphone`/`ipad`/`android`). When you use this package, use its helpers instead of reimplementing:\n\n```ts\nimport { isDespia, getPlatform } from '@despia/user-identity';\n\nconst inDespia = isDespia();           // same as userAgent.includes('despia')\nconst platform = getPlatform();       // 'ios' | 'android' | null (iphone/ipad vs android)\nconst isDespiaIOS = inDespia && platform === 'ios';\nconst isDespiaAndroid = inDespia && platform === 'android';\n// e.g. show RevenueCat in Despia, Stripe on web\n```\n\nSame behavior as the official doc: `isDespia()` checks for `\"despia\"`; `getPlatform()` returns `'ios'` (iphone/ipad) or `'android'`.\n\n---\n\n## Common scenarios\n\n| Scenario | What happens |\n|----------|--------------|\n| First install, no backup | `source: 'new'`, `appUserId` = install ID |\n| Reinstall, iCloud/Android backup restored | `source: 'vault'`, `appUserId` from backup |\n| Reinstall, no backup, but had purchases | `source: 'restore'`, `appUserId` from purchase history |\n| New device, same Apple/Google account, had purchases | `source: 'restore'` if purchase history syncs, else `source: 'new'` |\n| User taps \"Restore purchases\" | `restore()` returns recovered `appUserId` if any purchase had `externalUserId` |\n\n**Important**: For restore to work, you must pass `app_user_id` as `external_id` when launching RevenueCat paywalls. Otherwise purchases aren’t linked and recovery won’t find them.\n\n---\n\n## Troubleshooting\n\n| Issue | Likely cause |\n|-------|---------------|\n| `getPlatform` is undefined / not in package | Use `@despia/user-identity@1.0.1` or later. Import: `import { getPlatform } from '@despia/user-identity'`. Do not use a custom detectPlatform helper. |\n| Calls seem to hang / no callback in native | This package uses Despia’s promises directly—no extra timeout. Data is processed as soon as the native layer resolves (vault has a 30s timeout on the native side). Vault and install-ID fetch run in parallel. Install ID uses `window.uuid` first (set by native before any package loads), then `despia.uuid`, then the async get-uuid call only if needed. |\n| Getting `$RCAnonymous` instead of real ID | Recovery from purchase history prefers non–RevenueCat-anonymous `externalUserId` (ignores `$RCAnonymous` when a real ID exists). Ensure paywalls are launched with your `app_user_id` as `external_id` so RevenueCat stores it. |\n| `userIdentity()` returns null | Running in browser, not Despia native app. Use `isDespia()` to check. |\n| `restore()` returns null | No purchases, or purchases weren’t made with `external_id` (RevenueCat). Always pass `app_user_id` as `external_id` when launching paywalls. |\n| User “lost” after reinstall | iCloud/Android backup may not have synced yet, or backup was disabled. Restore purchases can still recover if they had a purchase with `externalUserId`. |\n\n---\n\n## API & response shapes\n\n### `userIdentity()` (alias: `getAppUserId()`)\n\nResolves identity and persists to vault. Call once at app launch.\n\n**Returns:** `IdentityResult | null`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `appUserId` | `string` | Main user ID. Use for RevenueCat `external_id`, OneSignal, backend. |\n| `installId` | `string` | Device/install UUID from `get-uuid://`. Changes per install. |\n| `source` | `'vault' \\| 'restore' \\| 'new'` | Where the ID came from: vault (iCloud/backup), restore (purchase history), or new (first install). |\n| `aliases` | `string[]` | Alternate IDs linked via `linkAlias()`. For fraud checks, analytics. |\n\n**Returns `null`** when not in Despia native runtime (e.g. web).\n\n```ts\n// Example\nconst result = await userIdentity();\n// { appUserId: \"abc-123\", installId: \"device-xyz\", source: \"vault\", aliases: [] }\n```\n\n---\n\n### `restore()`\n\nRecovers identity from purchase history. Use for \"Restore purchases\" button.\n\n**Returns:** `{ appUserId: string; aliases: string[] } | null`\n\n| Field | Type | Description |\n|-------|------|-------------|\n| `appUserId` | `string` | Recovered ID from a purchase with `externalUserId`. Sync to services. |\n| `aliases` | `string[]` | Current aliases (from vault). |\n\n**Returns `null`** when no purchases found, no purchase has `externalUserId`, or not in Despia.\n\n```ts\n// Example\nconst restored = await restore();\n// { appUserId: \"abc-123\", aliases: [] }  or  null\n```\n\n---\n\n### `setAppUserId(appUserId: string)`\n\nSets the main ID (e.g. from login). Persists to vault. No return value.\n\n---\n\n### `linkAlias(aliasId: string)`\n\nLinks an alternate ID to this user. Persists to vault. No return value. Skips if `aliasId` is already main or in aliases.\n\n---\n\n### `isDespia()`\n\n**Returns:** `boolean` - `true` if `navigator.userAgent` includes `\"despia\"` (native app), else `false`.\n\n---\n\n### `getPlatform()`\n\n**Returns:** `'ios' | 'android' | null` - platform when in Despia (based on user agent: iphone/ipad vs android). `null` on web or unknown.\n\n---\n\n## API summary\n\n| Function | Returns |\n|----------|---------|\n| `userIdentity()` | `{ appUserId, installId, source, aliases } \\| null` |\n| `restore()` | `{ appUserId, aliases } \\| null` |\n| `setAppUserId(id)` | `void` |\n| `linkAlias(id)` | `void` |\n| `isDespia()` | `boolean` |\n| `getPlatform()` | `'ios' \\| 'android' \\| null` |\n","readmeFilename":"README.md"}