despia-native 1.0.27 → 1.0.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +286 -712
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,168 +1,125 @@
1
- # Despia Native
1
+ # [Despia Native](https://despia.com)
2
2
 
3
- JavaScript SDK for [Despia](https://despia.com). Build with any web framework, access 50+ native device capabilities through a single JavaScript function, and publish to iOS and Android from a browser. No Swift, no Kotlin, no terminal.
3
+ Add 50+ native iOS and Android capabilities to any web app (React, Vue, Angular, Svelte, Next.js, vanilla JS) through a single `despia()` function. Plus native offline support, a local SQLite database, and an on-device streaming CDN, solving 15 years of hybrid-app problems with modern runtime architecture.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/despia-native)](https://www.npmjs.com/package/despia-native)
6
- [![license](https://img.shields.io/npm/l/despia-native)](https://github.com/despia-native/despia-native/blob/main/LICENSE)
7
6
 
8
- **[Documentation](https://setup.despia.com)** | **[AI Agent Index](https://setup.despia.com/llms.txt)** | **[iOS Deployment](https://setup.despia.com/deployment/apple-ios/automatic)** | **[Android Deployment](https://setup.despia.com/deployment/google-android/automatic)**
9
-
10
- Despia is the next generation of web-native development. Unlike earlier hybrid frameworks that routed files through JavaScript, forced Base64 encoding, imposed storage quotas, and ran on insecure `file://` origins, Despia runs your app on a real secure origin (`http://localhost` via Local Server, or your remote URL). Standard web APIs work without restrictions. File operations bypass JavaScript entirely and go directly to the native file system. A 500MB video file uses roughly 100 bytes of JS heap rather than 1.6GB.
11
-
12
- The runtime runs on WKWebView (iOS) and the Chromium-based WebView (Android) with hardware-accelerated compositing, JIT-compiled JavaScript, automatic DOM optimization, smart caching, and GPU-accelerated WebGL and Canvas 2D. CSS animations run on the compositor thread independently of JavaScript. The runtime has been actively maintained by the same core team since 2011, when it shipped as Advanced WebView. It has powered over 7,500 apps and became Despia in 2023. Native capabilities are implemented in Swift and Java and called from JavaScript through a single typed function. See the [changelog](https://setup.despia.com/changelog) for the full history.
7
+ **[Full documentation](https://setup.despia.com)**
13
8
 
14
9
  ---
15
10
 
16
- ## MCP Server
17
-
18
- Add the Despia MCP to give your AI assistant full knowledge of the `despia-native` API.
19
-
20
- [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://despia.com/mcp/cursor)
21
- [![Install in VS Code](https://img.shields.io/badge/Install_in_VS_Code-007ACC?logo=visualstudiocode&logoColor=white)](https://despia.com/mcp/vs-code)
22
-
23
- The badges use **HTTPS** links on [despia.com](https://despia.com) that redirect to the `cursor://` / `vscode:` installers (plain `cursor://` / `vscode:` links are not allowed as README `href` values on npm and GitHub).
24
-
25
- **[MCP Server setup guide (Cursor, VS Code, and more)](https://setup.despia.com/mcp-server)**
26
-
27
- **Cursor deeplink** (copy the full line):
11
+ ## Installation
28
12
 
29
- ```text
30
- cursor://anysphere.cursor-deeplink/mcp/install?name=Despia&config=eyJ0eXBlIjoiaHR0cCIsInVybCI6Imh0dHBzOi8vc2V0dXAuZGVzcGlhLmNvbS9tY3AifQ==
13
+ ```bash
14
+ npm install despia-native
15
+ # pnpm add despia-native
16
+ # yarn add despia-native
31
17
  ```
32
18
 
33
- **VS Code deeplink** (copy the full line):
34
-
35
- ```text
36
- vscode:mcp/install?%7B%22name%22%3A%22Despia%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fsetup.despia.com%2Fmcp%22%7D
19
+ ```js
20
+ import despia from 'despia-native';
37
21
  ```
38
22
 
39
- **Web-based builders** (Lovable, Bolt, v0, etc.) — paste the MCP URL where your tool asks for it:
23
+ Or via CDN:
40
24
 
41
- ```
42
- https://setup.despia.com/mcp
25
+ ```html
26
+ <!-- UMD -->
27
+ <script src="https://cdn.jsdelivr.net/npm/despia-native/index.min.js"></script>
43
28
  ```
44
29
 
45
- Look for "Add MCP", "MCP Settings", or "Personal Connectors" in your builder. Requires Node.js v18+ for local tools.
30
+ ```html
31
+ <!-- ESM -->
32
+ <script type="module">
33
+ import despia from 'https://cdn.jsdelivr.net/npm/despia-native/+esm';
34
+ </script>
35
+ ```
46
36
 
47
37
  ---
48
38
 
49
- ## Table of Contents
39
+ ## Quick Start
50
40
 
51
- - [MCP Server](#mcp-server)
52
- - [Installation](#installation)
53
- - [Quick Start](#quick-start)
54
- - [Environment Detection](#environment-detection)
55
- - [AI Agent Rules](#ai-agent-rules)
56
- - [API Reference](#api-reference)
57
- - [Deployment Models](#deployment-models)
58
- - [Features](#features)
59
- - [Haptic Feedback](#haptic-feedback)
60
- - [Identity Vault](#identity-vault)
61
- - [GPS Location](#gps-location)
62
- - [RevenueCat In-App Purchases](#revenuecat-in-app-purchases)
63
- - [AppsFlyer Attribution](#appsflyer-attribution)
64
- - [Push Notifications](#push-notifications)
65
- - [OAuth Authentication](#oauth-authentication)
66
- - [Clipboard](#clipboard)
67
- - [Contacts](#contacts)
68
- - [App Information and Device Data](#app-information-and-device-data)
69
- - [UI Controls and Styling](#ui-controls-and-styling)
70
- - [File and Media Operations](#file-and-media-operations)
71
- - [Apple Health (HealthKit)](#apple-health-healthkit)
72
- - [AdMob Inline Ads](#admob-inline-ads)
73
- - [Web Payment Request API](#web-payment-request-api)
74
- - [Web Storage APIs](#web-storage-apis)
75
- - [Local CDN](#local-cdn)
76
- - [Local Server](#local-server)
77
- - [Capability Reference](#capability-reference)
78
- - [Safe Area](#safe-area)
79
- - [Extending the Runtime](#extending-the-runtime)
80
- - [Web Apps vs React Native](#web-apps-vs-react-native)
81
- - [Publishing to the App Store and Google Play](#publishing-to-the-app-store-and-google-play)
82
- - [Open Source](#open-source)
83
- - [Support](#support)
84
- - [License](#license)
41
+ ```js
42
+ import despia from 'despia-native';
85
43
 
86
- ---
44
+ // Launch a RevenueCat paywall
45
+ despia(`revenuecat://launchPaywall?external_id=${userId}&offering=default`);
87
46
 
88
- ## Installation
47
+ // Link the device to your user for OneSignal push
48
+ despia(`setonesignalplayerid://?user_id=${userId}`);
89
49
 
90
- ```bash
91
- npm install despia-native
92
- # pnpm add despia-native
93
- # yarn add despia-native
94
- ```
50
+ // Store a session token behind Face ID / Touch ID (survives reinstall)
51
+ await despia('setvault://?key=sessionToken&value=abc123&locked=true');
95
52
 
96
- ```js
97
- import despia from 'despia-native';
53
+ // Read it back later (prompts biometric)
54
+ const { sessionToken } = await despia('readvault://?key=sessionToken', ['sessionToken']);
98
55
  ```
99
56
 
100
- > CDN alternative: `https://cdn.jsdelivr.net/npm/despia-native/+esm` (ESM) or `https://cdn.jsdelivr.net/npm/despia-native/index.min.js` (UMD, global)
57
+ No initialization. Open your app in the Despia runtime and it works.
101
58
 
102
59
  ---
103
60
 
104
- ## Quick Start
61
+ ## Why Despia
105
62
 
106
- ```js
107
- import despia from 'despia-native'; // npm / pnpm / yarn only
63
+ Despia is a hybrid app runtime built on the web platform. The web handles what it's good at (logic, UI, routing, state, storage); everything else (biometrics, push, in-app purchases, background GPS, system pickers) is handed to a native engine via a single `despia()` call.
108
64
 
109
- despia('successhaptic://'); // haptic feedback
110
- const device = await despia('get-uuid://', ['uuid']); // native device ID
111
- console.log(device.uuid);
112
- ```
65
+ Works with any web codebase. Bring an existing app or start fresh with any framework, any build tool, any host. No framework lock-in, no proprietary hosting, nothing to migrate.
113
66
 
114
- No initialization. No setup. Open your app in the Despia runtime and it works.
67
+ Full native offline support. [`@despia/local`](https://www.npmjs.com/package/@despia/local) serves your web build from `http://localhost`, [`@despia/powersync`](https://www.npmjs.com/package/@despia/powersync) adds a native SQLite database that syncs with your backend, and the [Local CDN](https://setup.despia.com/local-cdn) streams cached media from device storage. File transfers stream between native storage and the web layer without loading into the JS heap.
115
68
 
116
- **How it works under the hood**
69
+ Push, monetization, ads, and attribution are built in: OneSignal, RevenueCat, AdMob, AppsFlyer. Pre-wired into the runtime, configured from the Despia dashboard, called from web code. Builds, signing, and store submission run from the browser. No Xcode, no Android Studio.
117
70
 
118
- Calling `despia()` sets `window.despia` to the scheme string. On iOS this is intercepted in `WebViewController.swift` via `decidePolicyFor navigationAction`. On Android it is intercepted in `MainActivity.java` via `shouldOverrideUrlLoading`. Results are written back to the WebView as named window variables, which the SDK resolves as a promise.
71
+ ---
119
72
 
120
- ```js
121
- // despia('get-uuid://', ['uuid'])
122
- // 1. window.despia = 'get-uuid://' set by SDK
123
- // 2. window.location.href = 'get-uuid://' intercepted by native layer
124
- // 3. window.uuid = '<device-uuid>' written back to WebView
125
- const device = await despia('get-uuid://', ['uuid']);
126
- console.log(device.uuid);
127
- ```
73
+ ## Contents
128
74
 
129
- The SDK handles command queuing, promise-based variable watching with a 30-second timeout, and error handling.
75
+ - [Why Despia](#why-despia)
76
+ - [Environment Detection](#environment-detection)
77
+ - [For AI Coding](#for-ai-coding)
78
+ - [API Reference](#api-reference)
79
+ - [Deployment Models](#deployment-models)
80
+ - [Features](#features)
81
+ - [Capability Reference](#capability-reference)
82
+ - [Safe Area](#safe-area)
83
+ - [Extending the Runtime](#extending-the-runtime)
84
+ - [Web Native vs React Native](#web-native-vs-react-native)
85
+ - [Publishing](#publishing)
86
+ - [MCP Server](#mcp-server)
87
+ - [How It Works](#how-it-works)
88
+ - [Support](#support)
130
89
 
131
90
  ---
132
91
 
133
92
  ## Environment Detection
134
93
 
135
- The SDK only activates inside the Despia native runtime. Gate native calls using these checks:
94
+ The SDK only activates inside the Despia runtime. Gate native calls:
136
95
 
137
96
  ```js
138
97
  const isDespia = navigator.userAgent.toLowerCase().includes('despia');
139
98
 
140
- const isDespiaIOS = isDespia &&
141
- (navigator.userAgent.toLowerCase().includes('iphone') ||
142
- navigator.userAgent.toLowerCase().includes('ipad'));
99
+ const isDespiaIOS = isDespia && (
100
+ navigator.userAgent.toLowerCase().includes('iphone') ||
101
+ navigator.userAgent.toLowerCase().includes('ipad')
102
+ );
143
103
 
144
- const isDespiaAndroid = isDespia &&
145
- navigator.userAgent.toLowerCase().includes('android');
104
+ const isDespiaAndroid = isDespia && navigator.userAgent.toLowerCase().includes('android');
146
105
 
147
- if (isDespia) {
148
- despia('lighthaptic://');
149
- } else {
150
- // Fallback for browser or development environment
151
- }
106
+ if (isDespia) despia('lighthaptic://');
152
107
  ```
153
108
 
154
109
  ---
155
110
 
156
- ## AI Agent Rules
111
+ ## For AI Coding
157
112
 
158
- If you are an AI coding agent generating Despia app code, follow these rules:
113
+ Full API context: [`llms.txt`](https://setup.despia.com/llms.txt).
159
114
 
160
- - Import as a default: `import despia from 'despia-native'`
161
- - Do not write to `window.despia` directly; it is used internally by the SDK
162
- - Gate all native calls using the environment checks above
163
- - For offline (Local Server) apps, bundle the SDK via a package manager so it is available when the app is served from `http://localhost`
164
- - For Local CDN downloads, do not `await` `localcdn://write` with a watch key; use `window.contentServerChange(item)` or poll with `localcdn://read`
165
- - For the HTTP upload API, do not hardcode localhost ports; use `const host = window.location.host`
115
+ - Import as default: `import despia from 'despia-native'`
116
+ - Never write `window.despia` directly; the SDK uses it internally
117
+ - There is no `.init()`, `.ready()`, or `.setup()`; the SDK exports a single function with no initialization
118
+ - Do not mock or reimplement the SDK; mock implementations will not trigger the native runtime on real devices
119
+ - Gate native calls with the environment check above
120
+ - For Local Server apps, install via a package manager so the SDK ships with the build
121
+ - Do not `await` `localcdn://write` with a watch key; use `window.contentServerChange`
122
+ - For the upload API, read `window.location.host`; don't hardcode ports
166
123
 
167
124
  ---
168
125
 
@@ -170,847 +127,464 @@ If you are an AI coding agent generating Despia app code, follow these rules:
170
127
 
171
128
  ### `despia(command, watch?)`
172
129
 
130
+ | Parameter | Type | Description |
131
+ | --------- | ---------- | ---------------------------------------------- |
132
+ | `command` | `string` | A Despia protocol URL, e.g. `'lighthaptic://'` |
133
+ | `watch` | `string[]` | Variable names to wait for |
173
134
 
174
- | Parameter | Type | Description |
175
- | --------- | ---------- | -------------------------------------------------------------- |
176
- | `command` | `string` | A Despia protocol URL, e.g. `'lighthaptic://'` |
177
- | `watch` | `string[]` | Optional. Array of variable names to wait for in the response. |
178
-
179
-
180
- Returns a `Promise` that resolves when all watched variables are set by the native runtime.
135
+ Returns a `Promise` that resolves when all watched variables are set, with a 30-second timeout. Long-running work (downloads, purchases, biometric prompts) reports back via global callbacks like `window.onRevenueCatPurchase` or `window.contentServerChange` instead. `despia.variableName` is equivalent to `window.variableName`.
181
136
 
182
- **Timeout behavior**
183
-
184
- The SDK watches for a single variable for up to 30 seconds. This window applies to the variable watch only, not to the underlying native operation. Long-running operations — file downloads, purchases, biometric prompts — complete in native and deliver their result via a global callback function (e.g. `window.onRevenueCatPurchase`, `window.contentServerChange`) rather than a watched variable, so they are never constrained by this window. If a watched variable is never set, the Promise resolves with `undefined` and logs a timeout to the console.
185
-
186
- **Fresh-data behavior**
187
-
188
- Watched variables are cleared before each call to prevent resolving on stale values.
189
-
190
- - Single variable: `null` is treated as a valid resolved value. Other empty placeholders (`undefined`, `"n/a"`, `{}`, `[]`) are ignored.
191
- - Multiple variables: all variables must be non-null before the Promise resolves.
192
-
193
- **Direct property access**
194
-
195
- ```js
196
- despia.variableName // Equivalent to window.variableName
197
- ```
198
-
199
- ### Protocol format
200
-
201
- ```
202
- feature://action?param1=value1&param2=value2
203
- ```
137
+ Protocol format: `feature://action?param1=value1&param2=value2`
204
138
 
205
139
  ---
206
140
 
207
141
  ## Deployment Models
208
142
 
209
- **Remote hydration (default):** The binary ships without embedded web assets. On each launch, Despia fetches the current build from your configured hosting URL. Web content updates do not require App Store or Google Play resubmission.
210
-
211
- **Local Server:** For offline-first apps. Your web build is cached on-device and served from `http://localhost`. After initial hydration the app launches without a network request. See [Local Server](#local-server).
143
+ **Remote hydration (default).** Binary ships without web assets; Despia fetches the current build on launch. Web updates do not require store resubmission.
212
144
 
213
- **OTA version gating:** Gate features by minimum runtime version using `despia-version-guard`.
145
+ **Local Server.** Build is cached on-device and served from `http://localhost`. See [Local Server](#local-server).
214
146
 
215
- ```bash
216
- npm install despia-version-guard
217
- ```
147
+ **Version gating.** Use [`despia-version-guard`](https://www.npmjs.com/package/despia-version-guard) to gate features by minimum runtime version:
218
148
 
219
149
  ```jsx
220
150
  import { VersionGuard } from 'despia-version-guard';
221
151
 
222
- const MyApp = () => (
223
- <div>
224
- <StableFeature />
225
- <VersionGuard min_version="21.0.3">
226
- <NewFeature />
227
- </VersionGuard>
228
- </div>
229
- );
152
+ <VersionGuard min_version="21.0.3">
153
+ <NewFeature />
154
+ </VersionGuard>
230
155
  ```
231
156
 
232
157
  ---
233
158
 
234
159
  ## Features
235
160
 
236
- ### Haptic Feedback
237
-
238
- ```js
239
- despia('lighthaptic://'); // Subtle vibration
240
- despia('heavyhaptic://'); // Strong vibration
241
- despia('successhaptic://'); // Positive confirmation
242
- despia('warninghaptic://'); // Attention alert
243
- despia('errorhaptic://'); // Negative feedback
244
- ```
245
-
246
- [Read Full Documentation](https://setup.despia.com/native-features/haptic-feedback)
247
-
248
- ---
249
-
250
- ### Identity Vault
251
-
252
- Encrypted key-value storage backed by iCloud KV on iOS and Android App Backup on Android. Persists across uninstalls and reinstalls. Data syncs automatically across all devices sharing the same Apple ID or Google account.
253
-
254
- Set `locked=true` on any key to require Face ID or Touch ID before the value can be read back. Because the vault stores the actual value server-side and only returns it after biometric success, you can store real JWT tokens, session cookies, and API keys behind biometrics. This is a hardware-enforced security guarantee, not a client-side check that can be bypassed.
255
-
256
- ```js
257
- // Store a JWT token (reading it back requires Face ID / Touch ID)
258
- await despia('setvault://?key=sessionToken&value=abc123&locked=true');
259
-
260
- // Read triggers the biometric prompt; token is only returned on success
261
- const data = await despia('readvault://?key=sessionToken', ['sessionToken']);
262
- const token = data.sessionToken;
263
- ```
264
-
265
- If the key does not exist, `readvault://` throws. Wrap in try/catch to handle first-time users.
266
-
267
-
268
- | Parameter | Description |
269
- | --------- | ---------------------------------------------------------------- |
270
- | `key` | Storage key, e.g. `"userId"` or `"sessionToken"` |
271
- | `value` | String value to store |
272
- | `locked` | `"true"` requires biometrics on read. `"false"` for open access. |
273
-
274
-
275
- **Store a value without biometric protection**
276
-
277
- ```js
278
- await despia('setvault://?key=userId&value=user123&locked=false');
279
-
280
- const { userId } = await despia('readvault://?key=userId', ['userId']);
281
- ```
282
-
283
- **Protect a sensitive action with Face ID**
284
-
285
- ```js
286
- async function confirmWithBiometrics() {
287
- await despia('setvault://?key=confirm&value=yes&locked=true');
288
- try {
289
- const data = await despia('readvault://?key=confirm', ['confirm']);
290
- if (data.confirm === 'yes') {
291
- await performSensitiveAction();
292
- await despia('setvault://?key=confirm&value=&locked=false');
293
- }
294
- } catch {
295
- // User cancelled or biometric failed
296
- }
297
- }
298
- ```
299
-
300
- **Prevent free trial abuse**
301
-
302
- ```js
303
- async function checkTrialEligibility() {
304
- try {
305
- const data = await despia('readvault://?key=hasUsedTrial', ['hasUsedTrial']);
306
- return data.hasUsedTrial !== 'yes';
307
- } catch {
308
- // Key not found, first-time user
309
- await despia('setvault://?key=hasUsedTrial&value=yes&locked=false');
310
- return true;
311
- }
312
- }
313
- ```
314
-
315
- [Read Full Documentation](https://setup.despia.com/native-features/storage-vault) + [Read Full Documentation](https://setup.despia.com/native-features/biometrics)
316
-
317
- ---
318
-
319
- ### GPS Location
320
-
321
- ```js
322
- // Set up the live update callback
323
- window.onLocationChange = (data) => {
324
- if (!data.active) return;
325
- console.log(data.latitude, data.longitude, data.horizontalAccuracy);
326
- };
327
-
328
- // Start tracking (buffer in seconds, movement threshold in centimetres)
329
- despia('location://?buffer=60&movement=100');
330
-
331
- // Stop tracking and retrieve the session
332
- const { locationSession } = await despia('stoplocation://', ['locationSession']);
333
- ```
334
-
335
- **Server delivery**
336
-
337
- When `server` is set, each GPS point is POSTed to your endpoint as it is recorded. Server delivery, `window.onLocationChange`, and local session storage all run simultaneously and independently. Loss of network does not affect local storage or frontend callbacks.
338
-
339
- ```js
340
- despia('location://?server=https://api.example.com/track?user=USER_ID&buffer=30&movement=100');
341
- ```
342
-
343
- Each POST body matches the location object shape returned by `stoplocation://`.
344
-
345
- [Read Full Documentation](https://setup.despia.com/native-features/gps-location)
346
-
347
- ---
348
-
349
161
  ### RevenueCat In-App Purchases
350
162
 
351
- **Launch a paywall**
352
-
353
163
  ```js
164
+ // Paywall
354
165
  despia(`revenuecat://launchPaywall?external_id=${userId}&offering=default`);
355
- ```
356
166
 
357
-
358
- | Parameter | Required | Description |
359
- | ------------- | -------- | ------------------------------------------------------------------ |
360
- | `external_id` | Yes | Your user ID in RevenueCat |
361
- | `offering` | Yes | RevenueCat offering ID. Use `"default"` for your default offering. |
362
-
363
-
364
- **Direct purchase without paywall UI**
365
-
366
- ```js
367
- // iOS
167
+ // Direct purchase
368
168
  despia(`revenuecat://purchase?external_id=${userId}&product=monthly_premium_ios`);
369
-
370
- // Android
371
- despia(`revenuecat://purchase?external_id=${userId}&product=premium:monthly_premium_android`);
372
169
  ```
373
170
 
374
- **Handle purchase success**
375
-
376
- The Despia runtime calls `window.onRevenueCatPurchase()` immediately when the store confirms a transaction. This is a global function, not an event listener — part of how the bridge is designed to stay lightweight and offload heavy operations to the native layer.
377
-
378
- This callback is a signal that a transaction occurred, not confirmation that access should be granted. If you have a backend, the callback fires before your server has received and processed the RevenueCat webhook. Always wait for your backend to confirm before unlocking features.
379
-
380
- If you have no backend, use `getpurchasehistory://` inside the callback to check entitlements directly from the store:
171
+ `window.onRevenueCatPurchase()` fires when the store confirms a transaction. Verify entitlement before unlocking (via your backend webhook, or by reading the store directly):
381
172
 
382
173
  ```js
383
174
  window.onRevenueCatPurchase = async () => {
384
175
  const { restoredData } = await despia('getpurchasehistory://', ['restoredData']);
385
- const active = (restoredData ?? []).filter(p => p.isActive);
386
-
387
- if (active.some(p => p.entitlementId === 'premium')) unlockPremium();
388
- if (active.some(p => p.entitlementId === 'no_ads')) removeAds();
176
+ if (restoredData.some(p => p.isActive && p.entitlementId === 'premium')) unlockPremium();
389
177
  };
390
178
  ```
391
179
 
392
- **Restore purchases**
393
-
394
- ```js
395
- const { restoredData } = await despia('getpurchasehistory://', ['restoredData']);
396
- const hasPremium = restoredData
397
- .filter(p => p.isActive)
398
- .some(p => p.entitlementId === 'premium');
399
- ```
400
-
401
- Each purchase object includes `transactionId`, `productId`, `type`, `entitlementId`, `isActive`, `willRenew`, `purchaseDate`, `expirationDate`, `store`, `receipt`, and more. The response shape is normalized across iOS and Android.
402
-
403
- **Web fallback**
404
-
405
- Neither `launchPaywall` nor `purchase` work in a standard browser. Gate behind the environment check and fall back to a RevenueCat Web Purchase Link:
406
-
407
- ```js
408
- if (isDespia) {
409
- despia(`revenuecat://launchPaywall?external_id=${userId}&offering=default`);
410
- } else {
411
- window.location.href = `https://pay.rev.cat/<your_token>/${encodeURIComponent(userId)}`;
412
- }
413
- ```
180
+ In the browser, fall back to a RevenueCat Web Purchase Link.
414
181
 
415
- [Read Full Documentation](https://setup.despia.com/native-features/revenuecat/introduction) + [API reference](https://setup.despia.com/native-features/revenuecat/reference)
182
+ [Intro](https://setup.despia.com/native-features/revenuecat/introduction) · [Reference](https://setup.despia.com/native-features/revenuecat/reference)
416
183
 
417
184
  ---
418
185
 
419
- ### AppsFlyer Attribution
420
-
421
- Attribution data is recorded at install time by the native AppsFlyer SDK, cached on-device, and injected into the web layer on every page load. No requests, no waiting. Currently available on iOS, with Android coming soon.
422
-
423
- AppsFlyer must be enabled in **Despia > App > Settings > Integrations > AppsFlyer** with your dev key configured.
424
-
425
- **Attribution variables (injected on every page load)**
426
-
427
- ```js
428
- despia.appsFlyerAttribution // full attribution object (campaign, ad set, UTM params, etc.)
429
- despia.appsFlyerReferrer // normalized source: "tiktok_ad", "facebook_organic", "organic", etc.
430
- despia.appsFlyerUID // unique AppsFlyer user ID
431
- ```
432
-
433
- **User identification (call after login)**
434
-
435
- ```js
436
- despia('appsflyer://set_user_id?customer_user_id=' + encodeURIComponent(userId));
437
- despia('appsflyer://set_email?email=' + encodeURIComponent(email));
438
- despia('appsflyer://set_phone?phone=' + encodeURIComponent(phone)); // hashed with SHA256 automatically
439
- ```
440
-
441
- **GDPR consent**
442
-
443
- ```js
444
- // User accepted
445
- despia('appsflyer://set_consent?is_gdpr=true&has_consent=true');
446
-
447
- // User declined
448
- despia('appsflyer://set_consent?is_gdpr=true&has_consent=false');
449
- ```
450
-
451
- **Log in-app events**
186
+ ### Push Notifications
452
187
 
453
- Standard `af_` prefixed events map automatically to Meta and TikTok conversion events. Custom events appear in AppsFlyer for funnel and retention analysis.
188
+ Permission and OneSignal registration happen automatically at launch. Link the device to your user on every authenticated load:
454
189
 
455
190
  ```js
456
- // af_purchase maps to Meta Purchase and TikTok CompletePayment
457
- const purchase = { af_revenue: 9.99, af_currency: 'USD', af_content_id: 'pro_plan' };
458
- despia('appsflyer://log_event?event_name=af_purchase&event_values=' + encodeURIComponent(JSON.stringify(purchase)));
191
+ despia(`setonesignalplayerid://?user_id=${userId}`);
459
192
 
460
- // af_complete_registration maps to Meta CompleteRegistration
461
- const reg = { af_registration_method: 'email' };
462
- despia('appsflyer://log_event?event_name=af_complete_registration&event_values=' + encodeURIComponent(JSON.stringify(reg)));
193
+ // Check permission
194
+ const { nativePushEnabled } = await despia('checkNativePushPermissions://', ['nativePushEnabled']);
195
+ if (!nativePushEnabled) despia('settingsapp://');
463
196
 
464
- // Custom event (AppsFlyer only)
465
- const onboarding = { step: 1, name: 'select_interests' };
466
- despia('appsflyer://log_event?event_name=onboarding_step&event_values=' + encodeURIComponent(JSON.stringify(onboarding)));
197
+ // Local scheduled notification
198
+ despia('sendlocalpushmsg://push.send?s=60&msg=Hello&!#Title&!#https://myapp.com');
467
199
  ```
468
200
 
469
- `event_values` must always be `JSON.stringify()`ed and `encodeURIComponent()`ed. Event names are case-sensitive, lowercase alphanumeric and underscores only, max 300 unique names per day.
201
+ Configure OneSignal with **Native iOS** and **Native Android** as platforms, then send remote pushes from your backend with the OneSignal REST API.
470
202
 
471
- [Read Full Documentation](https://setup.despia.com/analytics/appsflyer/introduction) + [Read Full Documentation](https://setup.despia.com/analytics/appsflyer/attribution)
203
+ [Intro](https://setup.despia.com/native-features/onesignal/introduction) · [Reference](https://setup.despia.com/native-features/onesignal/reference)
472
204
 
473
205
  ---
474
206
 
475
- ### Push Notifications
207
+ ### Identity Vault
476
208
 
477
- Despia requests push permission and registers the device with OneSignal automatically at app launch. Call `setonesignalplayerid://` on every authenticated load to link the device to your user.
209
+ Encrypted key-value storage backed by iCloud KV (iOS) and Android App Backup. Values survive uninstall and sync across a user's own devices. Set `locked=true` to require Face ID, Touch ID, or fingerprint on read.
478
210
 
479
211
  ```js
480
- // Link device to your user (call on every authenticated app load)
481
- despia(`setonesignalplayerid://?user_id=${userId}`);
482
-
483
- // Send a local scheduled notification (fires after 60 seconds)
484
- despia('sendlocalpushmsg://push.send?s=60&msg=Hello&!#New Message&!#https://myapp.com');
212
+ // Write
213
+ await despia('setvault://?key=sessionToken&value=abc123&locked=true');
485
214
 
486
- // Check push permission status
487
- const result = await despia('checkNativePushPermissions://', ['nativePushEnabled']);
488
- if (!result.nativePushEnabled) {
489
- // Direct user to settings to enable notifications
490
- despia('settingsapp://');
491
- }
215
+ // Read (prompts biometric if locked)
216
+ const { sessionToken } = await despia('readvault://?key=sessionToken', ['sessionToken']);
492
217
  ```
493
218
 
494
- **Send to a specific user from your backend**
219
+ `readvault://` throws if the key doesn't exist. Wrap in try/catch.
495
220
 
496
- ```js
497
- await fetch('https://onesignal.com/api/v1/notifications', {
498
- method: 'POST',
499
- headers: {
500
- 'Content-Type': 'application/json',
501
- 'Authorization': 'Basic YOUR_REST_API_KEY',
502
- },
503
- body: JSON.stringify({
504
- app_id: 'ONESIGNAL-APP-ID',
505
- include_external_user_ids: [externalUserId],
506
- headings: { en: title },
507
- contents: { en: message },
508
- data: { url: 'https://yourapp.com/messages/123' } // navigate WebView on tap
509
- }),
510
- });
511
- ```
512
-
513
- When configuring OneSignal, select **Native iOS** and **Native Android** as the platforms. See [Read Full Documentation](https://setup.despia.com/native-features/onesignal/introduction) + [API reference](https://setup.despia.com/native-features/onesignal/reference) for setup, critical alerts (including Time Sensitive / Do Not Disturb), schemes, backend delivery, and targeting.
221
+ [Docs](https://setup.despia.com/native-features/storage-vault)
514
222
 
515
223
  ---
516
224
 
517
225
  ### OAuth Authentication
518
226
 
519
- The flow uses two Despia URL protocols. `oauth://` opens a secure browser session (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android). The `{scheme}://oauth/` prefix on the return deeplink tells Despia to close that session and navigate your WebView to the path that follows.
520
-
521
- When running in Despia, use a native-specific redirect URI pointing to `/native-callback.html` rather than your regular web auth callback. This is a different redirect URI from your web flow. Register both with your OAuth provider.
227
+ Opens a secure browser session (`ASWebAuthenticationSession` / Chrome Custom Tabs). A deeplink with the `oauth/` prefix closes the session and returns to your WebView.
522
228
 
523
229
  ```js
524
- const isDespia = navigator.userAgent.toLowerCase().includes('despia');
525
-
526
230
  const redirectUri = isDespia
527
231
  ? 'https://yourapp.com/native-callback.html'
528
232
  : 'https://yourapp.com/auth/callback';
529
233
 
530
- const oauthUrl = `https://provider.com/oauth/authorize?client_id=xxx&redirect_uri=${encodeURIComponent(redirectUri)}`;
234
+ const authUrl = `https://provider.com/oauth/authorize?client_id=xxx&redirect_uri=${encodeURIComponent(redirectUri)}`;
531
235
 
532
236
  if (isDespia) {
533
- // Step 1: open a secure native browser session
534
- despia(`oauth://?url=${encodeURIComponent(oauthUrl)}`);
237
+ despia(`oauth://?url=${encodeURIComponent(authUrl)}`);
535
238
  } else {
536
- // Regular web flow
537
- window.location.href = oauthUrl;
239
+ window.location.href = authUrl;
538
240
  }
539
241
  ```
540
242
 
541
- `/native-callback.html` runs inside the secure browser session. It receives the tokens or authorization code from the provider, handles the exchange if needed, then fires the deeplink to close the session and return to the app:
542
-
543
- ```js
544
- // Step 2: from inside the callback page, fire the deeplink to return to your app
545
- window.location.href = `{yourscheme}://oauth/auth?access_token=${token}`;
546
- ```
547
-
548
- Despia intercepts the deeplink, closes the browser session, and navigates your WebView to `/auth?access_token=xxx`.
549
-
550
- Deeplink format: `{yourscheme}://oauth/{path}?params`
551
-
552
- Your deeplink scheme is your app name in lowercase with no spaces (e.g. `myapp://`), or a custom Despialink set in the Despia editor.
553
-
554
-
555
- | Deeplink | Result |
556
- | -------------------------------------------- | ------------------------------------------------------------- |
557
- | `{yourscheme}://oauth/auth?access_token=xxx` | Browser closes, WebView navigates to `/auth?access_token=xxx` |
558
- | `{yourscheme}://oauth/home` | Browser closes, WebView navigates to `/home` |
559
- | `{yourscheme}://auth?access_token=xxx` | Browser stays open, user is stuck |
560
-
561
-
562
- **Callback page**
563
-
564
- Use a plain HTML file at `public/native-callback.html` rather than a React or Vue route. React Router can strip the `#access_token` hash fragment during a route change, causing tokens to disappear before your callback logic runs. A plain HTML file bypasses the router entirely and reads the hash directly from the browser.
243
+ In `public/native-callback.html` (plain HTML, not a framework route, so the hash isn't stripped on navigation):
565
244
 
566
245
  ```html
567
- <!-- public/native-callback.html -->
568
246
  <script>
569
- var params = new URLSearchParams(window.location.search);
570
- var hash = new URLSearchParams(window.location.hash.substring(1));
571
- var code = params.get('code'); // authorization code flow
572
- var accessToken = hash.get('access_token'); // implicit flow
573
-
574
- if (code) {
575
- // exchange code via your backend, then fire deeplink with tokens
576
- } else if (accessToken) {
577
- window.location.href = '{yourscheme}://oauth/auth?access_token=' + encodeURIComponent(accessToken);
578
- }
247
+ var hash = new URLSearchParams(window.location.hash.substring(1));
248
+ var token = hash.get('access_token');
249
+ if (token) window.location.href = '{yourscheme}://oauth/auth?access_token=' + encodeURIComponent(token);
579
250
  </script>
580
251
  ```
581
252
 
582
- **Already-mounted `/auth` page**
253
+ Deeplink format: `{yourscheme}://oauth/{path}?params`. Without the `oauth/` prefix the browser session stays open.
583
254
 
584
- When Despia navigates the WebView to `/auth`, if that route is already active your framework does not remount the component. Token-reading logic that only runs on mount will not fire again. Fix per framework:
585
-
586
- - React: include `searchParams` in your `useEffect` dependency array
587
- - Vue: use `watch: { '$route.query': { immediate: true, handler } }` instead of reading params in `mounted()`
588
- - Vanilla JS: call your handler on load and add `window.addEventListener('popstate', handler)`
589
-
590
- [Read Full Documentation](https://setup.despia.com/native-features/oauth/introduction)
255
+ [Docs](https://setup.despia.com/native-features/oauth/introduction)
591
256
 
592
257
  ---
593
258
 
594
- ### Clipboard
595
-
596
- ```js
597
- const { clipboarddata } = await despia('getclipboard://', ['clipboarddata']);
598
- ```
259
+ ### AppsFlyer Attribution
599
260
 
600
- [Read Full Documentation](https://setup.despia.com/native-features/clipboard)
261
+ iOS today, Android soon. Enable in **Despia > App > Settings > Integrations > AppsFlyer** with your dev key.
601
262
 
602
- ---
263
+ ```js
264
+ // Attribution available on every launch
265
+ despia.appsFlyerAttribution
266
+ despia.appsFlyerReferrer
267
+ despia.appsFlyerUID
603
268
 
604
- ### Contacts
269
+ // After login
270
+ despia('appsflyer://set_user_id?customer_user_id=' + encodeURIComponent(userId));
605
271
 
606
- ```js
607
- const { contacts } = await despia('readcontacts://', ['contacts']);
272
+ // Log an event (af_-prefixed events map to Meta and TikTok automatically)
273
+ const values = { af_revenue: 9.99, af_currency: 'USD' };
274
+ despia('appsflyer://log_event?event_name=af_purchase&event_values=' + encodeURIComponent(JSON.stringify(values)));
608
275
  ```
609
276
 
610
- [Read Full Documentation](https://setup.despia.com/native-features/contact-access)
277
+ [Intro](https://setup.despia.com/analytics/appsflyer/introduction) · [Attribution](https://setup.despia.com/analytics/appsflyer/attribution)
611
278
 
612
279
  ---
613
280
 
614
- ### App Information and Device Data
281
+ ### GPS Location
615
282
 
616
283
  ```js
617
- const { versionNumber, bundleNumber } = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
618
- const { uuid } = await despia('get-uuid://', ['uuid']);
619
- const { storeLocation } = await despia('getstorelocation://', ['storeLocation']);
620
- const { trackingDisabled } = await despia('user-disable-tracking://', ['trackingDisabled']);
284
+ window.onLocationChange = (data) => {
285
+ if (data.active) console.log(data.latitude, data.longitude);
286
+ };
287
+
288
+ // buffer in seconds, movement threshold in centimetres
289
+ despia('location://?buffer=60&movement=100');
290
+
291
+ const { locationSession } = await despia('stoplocation://', ['locationSession']);
621
292
  ```
622
293
 
623
- [Read Full Documentation](https://setup.despia.com/native-features/device-indexing) + [Read Full Documentation](https://setup.despia.com/native-features/store-location) + [Read Full Documentation](https://setup.despia.com/native-features/app-privacy)
294
+ Add `&server=https://api.example.com/track` to POST each point as it's recorded.
295
+
296
+ [Docs](https://setup.despia.com/native-features/gps-location)
624
297
 
625
298
  ---
626
299
 
627
- ### UI Controls and Styling
300
+ ### Apple Health (HealthKit)
301
+
302
+ iOS only. Gate behind `isDespiaIOS`. Permissions are requested on first call per identifier.
628
303
 
629
304
  ```js
630
- despia('spinneron://'); // Show loading spinner
631
- despia('spinneroff://'); // Hide loading spinner
632
- despia('hidebars://on'); // Hide status bar (full screen)
633
- despia('hidebars://off'); // Restore status bar
634
- despia('statusbarcolor://{255, 255, 255}'); // Set status bar background (RGB)
635
- despia('statusbartextcolor://{black}'); // Set status bar text color: black | white
636
- despia('settingsapp://'); // Open native app settings
637
- despia('reset://'); // Reset the app
305
+ // Read
306
+ const { healthkitResponse } = await despia(
307
+ 'readhealthkit://HKQuantityTypeIdentifierStepCount?days=7',
308
+ ['healthkitResponse']
309
+ );
310
+
311
+ // Write
312
+ despia('writehealthkit://HKQuantityTypeIdentifierBodyMass//74.5');
313
+
314
+ // Observe and POST to your backend on change
315
+ despia('healthkit://observe?types=HKQuantityTypeIdentifierStepCount&frequency=hourly&server=https://api.example.com/webhook');
638
316
  ```
639
317
 
640
- [Read Full Documentation](https://setup.despia.com/native-features/safe-areas) + [Read Full Documentation](https://setup.despia.com/native-features/app-settings)
318
+ Any `HKQuantityTypeIdentifier`, `HKCategoryTypeIdentifier`, `HKWorkoutTypeIdentifier`, or `HKCharacteristicTypeIdentifier` works.
319
+
320
+ [Docs](https://setup.despia.com/health-data/apple-health)
641
321
 
642
322
  ---
643
323
 
644
324
  ### File and Media Operations
645
325
 
646
- Despia automatically intercepts standard HTML file inputs and routes them to native UI. No Base64 blobs, no memory issues. File events are injected back into the standard HTML file input.
326
+ Standard HTML file inputs route to native UI with events delivered back to the input:
647
327
 
648
328
  ```html
649
- <!-- Opens a native action sheet with camera, document scanner, and media library -->
650
- <input type="file">
651
-
652
- <!-- Opens native media gallery for images -->
653
- <input type="file" accept="image/*">
654
-
655
- <!-- Opens native media gallery for video -->
656
- <input type="file" accept="video/*">
657
-
658
- <!-- Opens native camera directly -->
659
- <input type="file" accept="image/*" capture="environment">
329
+ <input type="file"> <!-- Action sheet -->
330
+ <input type="file" accept="image/*"> <!-- Image gallery -->
331
+ <input type="file" accept="video/*"> <!-- Video gallery -->
332
+ <input type="file" accept="image/*" capture="environment"><!-- Camera -->
660
333
  ```
661
334
 
662
- Additional file and media commands:
663
-
664
335
  ```js
665
336
  despia('takescreenshot://');
666
337
  despia('savethisimage://?url=https://example.com/image.jpg');
667
338
  despia('file://https://example.com/document.pdf');
668
- despia('shareapp://message?=Check%20out%20this%20app&url=https://myapp.com');
669
- despia('scanningmode://auto'); // auto | on | off
670
- ```
671
-
672
- [Read Full Documentation](https://setup.despia.com/native-features/file-sharing) + [Read Full Documentation](https://setup.despia.com/native-features/camera-roll)
673
-
674
- ---
675
-
676
- ### Apple Health (HealthKit)
677
-
678
- iOS only. Always gate behind `isDespiaIOS`. Despia requests permissions on the first call for each identifier.
679
-
680
- ```js
681
- // Read historical data
682
- const data = await despia('readhealthkit://HKQuantityTypeIdentifierStepCount?days=7', ['healthkitResponse']);
683
- const steps = data.healthkitResponse.HKQuantityTypeIdentifierStepCount;
684
- // [{ date: "2025-11-17", value: 9820, unit: "count" }, ...]
685
-
686
- // Write a value back to HealthKit
687
- despia('writehealthkit://HKQuantityTypeIdentifierBodyMass//74.5');
688
-
689
- // Observe changes and POST to your server whenever HealthKit updates
690
- despia('healthkit://observe?types=HKQuantityTypeIdentifierStepCount&frequency=hourly&server=https://api.example.com/webhook?user=USER_ID');
339
+ despia('shareapp://message?=Check%20this%20out&url=https://myapp.com');
691
340
  ```
692
341
 
693
- Pass any valid `HKQuantityTypeIdentifier`, `HKCategoryTypeIdentifier`, `HKWorkoutTypeIdentifier`, or `HKCharacteristicTypeIdentifier` directly. Sleep, workouts, heart rate, body mass, and all other HealthKit types are supported. [Read Full Documentation](https://setup.despia.com/health-data/apple-health).
342
+ [File sharing](https://setup.despia.com/native-features/file-sharing) · [Camera roll](https://setup.despia.com/native-features/camera-roll)
694
343
 
695
344
  ---
696
345
 
697
346
  ### AdMob Inline Ads
698
347
 
699
- Despia implements Google's WebView API for Ads on both platforms, connecting the Mobile Ads SDK directly to the WebView so AdMob ads render as real DOM elements inside your web UI, not as native overlays. Enabled from **Despia > App > Settings > AdMob**. Requires a rebuild after enabling.
700
-
701
- Once enabled, serve ads using standard AdSense, Google Publisher Tag, or IMA for HTML5 tags. No Despia SDK calls needed on the web side.
702
-
703
- ```html
704
- <!-- AdSense banner -->
705
- <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
706
- data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"></script>
707
- <ins class="adsbygoogle"
708
- style="display:block"
709
- data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
710
- data-ad-slot="YYYYYYYYYY"
711
- data-ad-format="auto"
712
- data-full-width-responsive="true"></ins>
713
- <script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
714
- ```
715
-
716
- Because ads are real DOM elements, placements that are impossible with native overlay ads — banners between feed cards, mid-article units, rewarded content gates, pre-roll video — are straightforward CSS and JavaScript.
348
+ Ads render as real DOM elements via Google's WebView API for Ads. Enable in **Despia > App > Settings > AdMob**, then use standard AdSense, Google Publisher Tag, or IMA tags. No SDK calls from web.
717
349
 
718
- Google WebView API for Ads references: [iOS](https://developers.google.com/admob/ios/browser/webview/api-for-ads) | [Android](https://developers.google.com/admob/android/browser/webview/api-for-ads) | [Read Full Documentation](https://setup.despia.com/native-features/admob/inline-ads) + [Read Full Documentation](https://setup.despia.com/native-features/admob/rewarded-ads)
350
+ [Inline ads](https://setup.despia.com/native-features/admob/inline-ads) · [Rewarded](https://setup.despia.com/native-features/admob/rewarded-ads)
719
351
 
720
352
  ---
721
353
 
722
354
  ### Web Payment Request API
723
355
 
724
- Despia polyfills support for the Web Payment Request API, enabling native Apple Pay on iOS and Google Pay on Android directly from your web UI. This is a known pain point with standard WebViews — Despia handles it by importing WebKit's Payment Request support on Android (`androidx.webkit:webkit:1.14.0` with `WebSettingsCompat.setPaymentRequestEnabled`) and the required Google Pay intent queries, and on iOS via WKWebView's built-in Apple Pay support.
356
+ Apple Pay and Google Pay work through the standard Payment Request API with no Despia-specific calls.
725
357
 
726
358
  ```js
727
- // Standard Web Payment Request API — works in Despia without any native code changes
728
359
  const request = new PaymentRequest(
729
- [{ supportedMethods: 'https://apple.com/apple-pay' }], // or Google Pay method
730
- {
731
- total: { label: 'Total', amount: { currency: 'USD', value: '9.99' } }
732
- }
360
+ [{ supportedMethods: 'https://apple.com/apple-pay' }],
361
+ { total: { label: 'Total', amount: { currency: 'USD', value: '9.99' } } }
733
362
  );
734
-
735
- const result = await request.show();
736
- await result.complete('success');
363
+ await (await request.show()).complete('success');
737
364
  ```
738
365
 
739
- Apple Pay on iOS and Google Pay on Android both work via the standard Web Payment Request API. No Despia-specific calls required. [Read Full Documentation](https://setup.despia.com/native-features/external-links) + [Google Pay in Android WebView](https://developers.google.com/pay/api/android/guides/recipes/using-android-webview)
366
+ [Docs](https://setup.despia.com/native-features/external-links)
740
367
 
741
368
  ---
742
369
 
743
- ### Web Storage APIs
370
+ ### Local Server
744
371
 
745
- Despia runs your app on a secure origin (`http://localhost` via Local Server, or your remote URL). This means all standard web storage APIs work without any restrictions or workarounds, unlike other hybrid frameworks that require hacks or fall back to `file://` origins:
372
+ Caches the web build on-device and serves it from `http://localhost` for offline launch. Updates are detected and swapped atomically on the next launch.
373
+
374
+ ```bash
375
+ npm install --save-dev @despia/local
376
+ ```
746
377
 
747
378
  ```js
748
- // localStorage works normally
749
- localStorage.setItem('userId', 'user123');
750
- const userId = localStorage.getItem('userId');
751
-
752
- // IndexedDB works normally
753
- const db = await indexedDB.open('myapp', 1);
754
-
755
- // Web Crypto works normally
756
- const key = await crypto.subtle.generateKey(
757
- { name: 'AES-GCM', length: 256 },
758
- true,
759
- ['encrypt', 'decrypt']
760
- );
379
+ // vite.config.js (also Webpack, Rollup, Nuxt, SvelteKit, Astro, Remix, esbuild)
380
+ import { despiaLocalPlugin } from '@despia/local/vite';
381
+
382
+ export default {
383
+ plugins: [despiaLocalPlugin({ outDir: 'dist', entryHtml: 'index.html' })],
384
+ };
761
385
  ```
762
386
 
763
- For data that needs to survive uninstall and reinstall, or be locked behind Face ID, use [Identity Vault](#identity-vault) instead.
387
+ Or run `npx despia-local dist` after any build. Deploy as normal.
764
388
 
765
- [Read Full Documentation](https://setup.despia.com/local-server/introduction) + [Read Full Documentation](https://setup.despia.com/native-features/simple-storage)
389
+ [Intro](https://setup.despia.com/local-server/introduction) · [Reference](https://setup.despia.com/local-server/reference)
766
390
 
767
391
  ---
768
392
 
769
- ### Local Server
770
-
771
- > **Built-in OTA updates. True offline support. Host with your existing web hosting. No proprietary infrastructure required.**
393
+ ### Local CDN
772
394
 
773
- No service workers. No workarounds. The Local Server downloads your complete web build to the device and serves it from a native on-device HTTP server at `http://localhost`. The app loads from device storage at native speed and works completely offline from the first launch after hydration.
395
+ Cache remote files on-device for offline playback and background downloads. Transfers use `NSURLSession` / `WorkManager` and continue when the app is closed, with Live Activity (iOS) or system tray (Android) progress.
774
396
 
775
- When the user has a connection, Despia checks for updates in the background by comparing the `deployed_at` timestamp in your manifest. If a new build is available it downloads silently while the user continues using the app. The update applies on the next launch. If the connection is not stable, the locally cached version is served without interruption.
397
+ ```js
398
+ // Called on download complete
399
+ window.contentServerChange = (item) => {
400
+ // item.local_cdn, item.cdn, item.index, item.size, item.status, item.local_path
401
+ };
776
402
 
777
- You host the manifest and assets on your own infrastructure — Netlify, Vercel, AWS, your own server, anywhere. No proprietary hosting, no per-MAU fees, no vendor lock-in on your delivery pipeline.
403
+ // Start a download. Do NOT await with a watch key.
404
+ despia(`localcdn://write?url=${remoteUrl}&filename=videos/clip.mp4&index=clip_1`);
778
405
 
779
- ```bash
780
- npm install --save-dev @despia/local
406
+ // Query cache
407
+ const { cdnItems } = await despia('localcdn://query', ['cdnItems']);
781
408
  ```
782
409
 
783
- ```js
784
- // vite.config.js (also available for Webpack, Rollup, Nuxt, SvelteKit, Astro, Remix, esbuild)
785
- import { defineConfig } from 'vite';
786
- import { despiaLocalPlugin } from '@despia/local/vite';
410
+ Play cached media from the `local_cdn` URL. For uploads, POST to `http://${window.location.host}/api/upload` (Local Server only).
787
411
 
788
- export default defineConfig({
789
- plugins: [
790
- despiaLocalPlugin({
791
- outDir: 'dist',
792
- entryHtml: 'index.html'
793
- })
794
- ]
795
- });
796
- ```
412
+ [Intro](https://setup.despia.com/local-cdn/introduction) · [Reference](https://setup.despia.com/local-cdn/reference)
797
413
 
798
- Or run via CLI after any build:
414
+ ---
799
415
 
800
- ```bash
801
- npx despia-local dist
802
- ```
416
+ ### Web Storage APIs
803
417
 
804
- The plugin generates `despia/local.json` in your output directory alongside your assets. Deploy both to your existing web host. That is the entire setup.
418
+ The app runs on a real origin, so `localStorage`, `IndexedDB`, and Web Crypto work normally. For data that must survive uninstall or be locked behind biometrics, use [Identity Vault](#identity-vault).
805
419
 
806
- ```json
807
- {
808
- "entry": "/index.html",
809
- "deployed_at": "1737225600000",
810
- "assets": [
811
- "/index.html",
812
- "/assets/app.abc123.css",
813
- "/assets/app.def456.js"
814
- ]
815
- }
420
+ ---
421
+
422
+ ### Haptic Feedback
423
+
424
+ ```js
425
+ despia('lighthaptic://'); // Subtle
426
+ despia('heavyhaptic://'); // Strong
427
+ despia('successhaptic://'); // Success
428
+ despia('warninghaptic://'); // Warning
429
+ despia('errorhaptic://'); // Error
816
430
  ```
817
431
 
818
- [Read Full Documentation](https://setup.despia.com/local-server/introduction) + [API reference](https://setup.despia.com/local-server/reference)
432
+ [Docs](https://setup.despia.com/native-features/haptic-feedback)
819
433
 
820
434
  ---
821
435
 
822
- ### Local CDN
823
-
824
- Cache individual remote files on-device for offline playback and background downloads. Downloads use native OS transfer APIs (NSURLSession on iOS, WorkManager on Android) and continue when the app is closed, with automatic retry on network failure. On iOS a Live Activity shows real-time download progress. On Android a native notification appears in the system tray. Both require no setup.
436
+ ### UI Controls and Styling
825
437
 
826
438
  ```js
827
- // Called by the native runtime when a download completes.
828
- // This is a global function, not an event listener.
829
- window.contentServerChange = (item) => {
830
- // item.local_cdn - localhost URL to use for playback
831
- // item.cdn - original remote URL
832
- // item.index - the uniqueId you passed to the write call
833
- // item.size - file size in bytes
834
- // item.status - "cached" when complete
835
- // item.local_path - absolute path on the device
836
-
837
- console.log('Cached:', item.index, item.local_cdn);
838
- addToDownloadsList(item);
839
- };
439
+ despia('spinneron://');
440
+ despia('spinneroff://');
441
+ despia('hidebars://on'); // Hide status bar
442
+ despia('hidebars://off');
443
+ despia('statusbarcolor://{255, 255, 255}'); // RGB
444
+ despia('statusbartextcolor://{black}'); // black | white
445
+ despia('settingsapp://'); // Open native app settings
446
+ despia('reset://');
447
+ ```
840
448
 
841
- // Fire and forget. Do not await with a watch key; large downloads take longer than the SDK's
842
- // 30-second variable-watch window. Use window.contentServerChange to receive the result instead.
843
- despia(
844
- `localcdn://write?url=${remoteUrl}&filename=videos/clip.mp4&index=clip_1&push=true&pushmessage="Download complete"`
845
- );
449
+ [Safe areas](https://setup.despia.com/native-features/safe-areas) · [App settings](https://setup.despia.com/native-features/app-settings)
846
450
 
847
- // Read cached items by ID
848
- const { cdnItems } = await despia(
849
- `localcdn://read?index=${encodeURIComponent(JSON.stringify(['clip_1']))}`,
850
- ['cdnItems']
851
- );
451
+ ---
452
+
453
+ ### App Information and Device Data
852
454
 
853
- // Or query everything in the cache
854
- const { cdnItems: all } = await despia('localcdn://query', ['cdnItems']);
455
+ ```js
456
+ const { versionNumber, bundleNumber } = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
457
+ const { uuid } = await despia('get-uuid://', ['uuid']);
458
+ const { storeLocation } = await despia('getstorelocation://', ['storeLocation']);
459
+ const { trackingDisabled } = await despia('user-disable-tracking://', ['trackingDisabled']);
855
460
  ```
856
461
 
857
- Use the `local_cdn` URL for playback:
462
+ [Device indexing](https://setup.despia.com/native-features/device-indexing) · [Store location](https://setup.despia.com/native-features/store-location) · [App privacy](https://setup.despia.com/native-features/app-privacy)
858
463
 
859
- ```html
860
- <video src="http://localhost:7777/localcdn/videos/clip.mp4" controls></video>
861
- ```
464
+ ---
862
465
 
863
- **HTTP upload API** (Local Server only)
466
+ ### Clipboard
864
467
 
865
468
  ```js
866
- const host = window.location.host; // Do not hardcode the port; it rotates per session
867
- const fd = new FormData();
868
- fd.append('file', fileInput.files[0]);
869
-
870
- const res = await fetch(`http://${host}/api/upload`, { method: 'POST', body: fd });
871
- const data = await res.json();
872
- // { success: true, fileName: "video.mp4", url: "http://localhost:7777/files/video.mp4" }
469
+ const { clipboarddata } = await despia('getclipboard://', ['clipboarddata']);
873
470
  ```
874
471
 
472
+ [Docs](https://setup.despia.com/native-features/clipboard)
473
+
474
+ ---
875
475
 
876
- | Method | Storage Path | URL Pattern |
877
- | ------------------ | ------------ | -------------------------------------- |
878
- | `localcdn://write` | `/localcdn/` | `localhost:{PORT}/localcdn/{filepath}` |
879
- | `/api/upload` | `/files/` | `localhost:{PORT}/files/{filename}` |
476
+ ### Contacts
880
477
 
478
+ ```js
479
+ const { contacts } = await despia('readcontacts://', ['contacts']);
480
+ ```
881
481
 
882
- [Read Full Documentation](https://setup.despia.com/local-cdn/introduction) + [API reference](https://setup.despia.com/local-cdn/reference)
482
+ [Docs](https://setup.despia.com/native-features/contact-access)
883
483
 
884
484
  ---
885
485
 
886
486
  ## Capability Reference
887
487
 
888
- A full index of native capabilities built into Despia.
889
-
890
- **Core runtime:** Hardware-backed Identity Vault, Face ID / Touch ID / fingerprint biometrics, background GPS (including Samsung, Huawei, Xiaomi, and OnePlus), contacts, clipboard, haptics (5 types), native file system access, image saving, background audio, local push notifications, status bar controls, safe area CSS variables, device orientation per device class, prevent zoom, prevent sleep, fullscreen mode, splash screen, iOS Home Widgets, Siri Shortcuts, native share dialog, AirPrint, screen shield, PkPass for iOS and Android mobile wallets.
488
+ **Core.** Identity Vault, Face ID / Touch ID / fingerprint, background GPS, contacts, clipboard, haptics, native file system, image saving, background audio, local push, status bar controls, safe area CSS variables, device orientation, zoom lock, sleep lock, fullscreen, splash screen, iOS Home Widgets, Siri Shortcuts, native share, AirPrint, screen shield, PkPass for mobile wallets.
891
489
 
892
- **Integrated SDK bridges:** RevenueCat (purchases, subscriptions, restore, paywalls), OneSignal (remote push with external user ID mapping), AppsFlyer (attribution, deep linking, event tracking), AdMob (advertising), HealthKit (all major health identifiers).
490
+ **SDK bridges.** RevenueCat, OneSignal, AppsFlyer, AdMob, HealthKit.
893
491
 
894
- **Infrastructure:** ATT compliance, vendor ID tracking, device ID tracking via iCloud KV and Android App Backup, store location access, jailbreak detection with configurable blocking, App Clips, Share Extensions, and Home Widget management from the Despia editor.
492
+ **Infrastructure.** ATT compliance, vendor ID, store location, jailbreak detection, App Clips, Share Extensions, Home Widget configuration.
895
493
 
896
- **Native web interception:** `<input type="file">` routes to a native action sheet. The `capture` attribute opens the native camera. `accept="image/*"` or `accept="video/*"` opens the native media gallery. Deeplinks and HTTPS deeplinks are handled natively.
494
+ **Web interception.** `<input type="file">` routes to the native action sheet; `capture` opens the camera; `accept` filters to media gallery. Deeplinks and HTTPS deeplinks are handled natively.
897
495
 
898
496
  ---
899
497
 
900
498
  ## Safe Area
901
499
 
902
- Despia exposes top and bottom safe area insets as CSS custom properties set by the native runtime.
500
+ Top and bottom safe area insets are exposed as CSS custom properties:
903
501
 
904
502
  ```css
905
- .header {
906
- padding-top: var(--safe-area-top);
907
- }
908
-
909
- .footer {
910
- padding-bottom: var(--safe-area-bottom);
911
- }
912
-
913
- .full-height {
914
- height: calc(100vh - var(--safe-area-top) - var(--safe-area-bottom));
915
- }
503
+ .header { padding-top: var(--safe-area-top); }
504
+ .footer { padding-bottom: var(--safe-area-bottom); }
916
505
  ```
917
506
 
918
- Note: left and right safe area variables are not available.
507
+ Left and right insets are not exposed.
919
508
 
920
- [Read Full Documentation](https://setup.despia.com/native-features/safe-areas)
509
+ [Docs](https://setup.despia.com/native-features/safe-areas)
921
510
 
922
511
  ---
923
512
 
924
513
  ## Extending the Runtime
925
514
 
926
- A common concern when adopting any hybrid framework is lock-in: can you escape the custom protocol API later?
927
-
928
- The bridge pattern is consistent across both platforms and fully open. You can intercept any custom scheme in `WebViewController.swift` on iOS or `MainActivity.java` on Android, run native code, and write the result back to the WebView. The SDK resolves custom bridges identically to built-in capabilities. No special registration or plugin system required.
929
-
930
- **On the bridge architecture**
931
-
932
- The scheme bridge is simple by design and has been running in production since 2011. A command goes in as a plain string, the native layer acts on it, and the result is written back as a named window variable. No serialization, no abstraction layers. Input is sanitized on the native side.
933
-
934
- For file uploads, streaming, and binary data, Despia uses the on-device HTTP server at `http://localhost` rather than the scheme bridge. The two channels handle what each is suited for.
935
-
936
- A typed plugin system for community contributions and custom native development is in progress, alongside the open source Extension system planned for mid/late 2026.
937
-
938
- **iOS: WebViewController.swift**
515
+ Intercept any custom scheme in `WebViewController.swift` (iOS) or `MainActivity.java` (Android), run native code, write the result back as a window variable. The SDK resolves it the same way as built-in schemes.
939
516
 
940
517
  ```swift
518
+ // iOS
941
519
  if requestURL.absoluteString.hasPrefix("mycustom://") {
942
- let result = runMyNativeCode()
943
- webView.evaluateJavaScript("window.myResult = '\(result)';")
520
+ webView.evaluateJavaScript("window.myResult = '\(runMyNativeCode())';")
944
521
  decisionHandler(.cancel)
945
522
  return
946
523
  }
947
524
  ```
948
525
 
949
- **Android: MainActivity.java**
950
-
951
526
  ```java
527
+ // Android
952
528
  if (url.startsWith("mycustom://")) {
953
- String result = runMyNativeCode();
954
- webView.evaluateJavascript("window.myResult = '" + result + "';", null);
529
+ webView.evaluateJavascript("window.myResult = '" + runMyNativeCode() + "';", null);
955
530
  return true;
956
531
  }
957
532
  ```
958
533
 
959
- **Call it from your web app**
960
-
961
534
  ```js
962
- import despia from 'despia-native';
963
-
964
- const data = await despia('mycustom://', ['myResult']);
965
- console.log(data.myResult);
535
+ // Web
536
+ const { myResult } = await despia('mycustom://', ['myResult']);
966
537
  ```
967
538
 
968
- Adding custom native code requires exporting the project and deploying outside of Despia's hosted CI/CD pipeline. Despia offers full Xcode and Android Studio project export for developers who need it. You own the code and can build and ship entirely independently at any point.
539
+ Full Xcode and Android Studio project export is available for custom native code.
969
540
 
970
- Each lifetime Despia license applies to one specific Despia project bound to one bundle ID, and grants export, editing, self-hosting, and custom CI/CD rights for that project only. Associated repositories must remain private. Reuse beyond that scope requires separate licensing or white-label terms.
541
+ ---
971
542
 
972
- Projects and their licensing rights can be transferred between Despia accounts. Contact [license@despia.com](mailto:license@despia.com) for transfers or licensing questions, and [whitelabel@despia.com](mailto:whitelabel@despia.com) for white-label use cases.
543
+ ## Web Native vs React Native
973
544
 
974
- > **Coming mid/late 2026:** An official open source Despia Extension system is currently in active development. It will allow developers to build and distribute custom native capabilities that work directly within the Despia editor, with no Xcode or Android Studio required.
545
+ This SDK is for web-native apps (React, Vue, Angular, Svelte, Next.js, Vite, Nuxt, vanilla JS) running inside the Despia runtime. It is not for React Native, Expo, or native mobile development.
975
546
 
976
547
  ---
977
548
 
978
- ## Web Apps vs React Native
549
+ ## Publishing
979
550
 
980
- This SDK is for web apps running inside the Despia runtime: React, Vue, Angular, Svelte, Next.js, Vite, Nuxt, and vanilla JavaScript.
551
+ iOS and Android store deployment runs from the web editor. Cloud Mac Mini infrastructure handles code signing, provisioning, and submission.
981
552
 
982
- It is not for React Native, Expo, or native mobile development.
553
+ [iOS](https://setup.despia.com/deployment/apple-ios/automatic) · [Android](https://setup.despia.com/deployment/google-android/automatic)
983
554
 
984
555
  ---
985
556
 
986
- ## Publishing to the App Store and Google Play
557
+ ## MCP Server
987
558
 
988
- Despia provides fully automated iOS and Android store deployment from the web editor. No Mac required. One-click deployment spins up Mac Mini build infrastructure in the cloud, handles code signing, provisioning, and submits to both the App Store and Google Play — entirely from a browser.
559
+ Point your AI assistant at the Despia MCP for the full `despia-native` API.
989
560
 
990
- [Read Full Documentation](https://setup.despia.com/deployment/apple-ios/automatic) + [Read Full Documentation](https://setup.despia.com/deployment/google-android/automatic)
561
+ [![Install in Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://despia.com/mcp/cursor)
562
+ [![Install in VS Code](https://img.shields.io/badge/Install_in_VS_Code-007ACC?logo=visualstudiocode&logoColor=white)](https://despia.com/mcp/vs-code)
991
563
 
992
- ---
564
+ For web builders (Lovable, Bolt, v0), paste: `https://setup.despia.com/mcp`.
993
565
 
994
- ## Open Source
566
+ [Setup guide](https://setup.despia.com/mcp-server)
995
567
 
568
+ ---
996
569
 
997
- | Package | Description | License |
998
- | -------------------------------------------------------------------------- | --------------------- | ------- |
999
- | [despia-native](https://www.npmjs.com/package/despia-native) | JavaScript SDK | MIT |
1000
- | [@despia/local](https://www.npmjs.com/package/@despia/local) | Offline asset bundler | MIT |
1001
- | [despia-version-guard](https://www.npmjs.com/package/despia-version-guard) | OTA version gating | MIT |
570
+ ## How It Works
1002
571
 
572
+ The SDK is a thin wrapper over a setter-based protocol bridge. Calling `despia()` assigns the scheme string to `window.despia`, which the native layer intercepts:
1003
573
 
1004
- Native capability implementations are written in Swift and Java and included in full on project export.
574
+ ```js
575
+ // When you call:
576
+ despia('lighthaptic://');
1005
577
 
1006
- ---
578
+ // the SDK does:
579
+ window.despia = 'lighthaptic://';
580
+ ```
1007
581
 
1008
- ## Support
582
+ iOS intercepts the assignment in `WebViewController.swift`; Android intercepts it in `MainActivity.java`. Results come back as named window variables, which the SDK resolves as a promise via the optional `watch` array. For long-running work (downloads, purchases, biometric prompts), the native layer calls back via global functions like `window.onRevenueCatPurchase` or `window.contentServerChange` instead.
1009
583
 
1010
- For questions or concerns regarding this package, contact the Despia team at [npm@despia.com](mailto:npm@despia.com).
584
+ No dependencies, no initialization, no lifecycle to manage.
1011
585
 
1012
586
  ---
1013
587
 
1014
- ## License
588
+ ## Support
1015
589
 
1016
- MIT
590
+ [npm@despia.com](mailto:npm@despia.com)