despia-native 1.0.26 → 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 +288 -699
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,72 +1,10 @@
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.
13
-
14
- ---
15
-
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**](cursor://anysphere.cursor-deeplink/mcp/install?name=Despia&config=eyJ0eXBlIjoiaHR0cCIsInVybCI6Imh0dHBzOi8vc2V0dXAuZGVzcGlhLmNvbS9tY3AifQ==) · [**Install in VS Code**](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)
21
-
22
- Full manual steps and other editors: [MCP Server setup guide](https://setup.despia.com/mcp-server).
23
-
24
- **Web-based builders** (Lovable, Bolt, v0, etc.) — paste the MCP URL where your tool asks for it:
25
-
26
- ```
27
- https://setup.despia.com/mcp
28
- ```
29
-
30
- Look for "Add MCP", "MCP Settings", or "Personal Connectors" in your builder. Requires Node.js v18+ for local tools.
31
-
32
- ---
33
-
34
- ## Table of Contents
35
-
36
- - [MCP Server](#mcp-server)
37
- - [Installation](#installation)
38
- - [Quick Start](#quick-start)
39
- - [Environment Detection](#environment-detection)
40
- - [AI Agent Rules](#ai-agent-rules)
41
- - [API Reference](#api-reference)
42
- - [Deployment Models](#deployment-models)
43
- - [Features](#features)
44
- - [Haptic Feedback](#haptic-feedback)
45
- - [Identity Vault](#identity-vault)
46
- - [GPS Location](#gps-location)
47
- - [RevenueCat In-App Purchases](#revenuecat-in-app-purchases)
48
- - [AppsFlyer Attribution](#appsflyer-attribution)
49
- - [Push Notifications](#push-notifications)
50
- - [OAuth Authentication](#oauth-authentication)
51
- - [Clipboard](#clipboard)
52
- - [Contacts](#contacts)
53
- - [App Information and Device Data](#app-information-and-device-data)
54
- - [UI Controls and Styling](#ui-controls-and-styling)
55
- - [File and Media Operations](#file-and-media-operations)
56
- - [Apple Health (HealthKit)](#apple-health-healthkit)
57
- - [AdMob Inline Ads](#admob-inline-ads)
58
- - [Web Payment Request API](#web-payment-request-api)
59
- - [Web Storage APIs](#web-storage-apis)
60
- - [Local CDN](#local-cdn)
61
- - [Local Server](#local-server)
62
- - [Capability Reference](#capability-reference)
63
- - [Safe Area](#safe-area)
64
- - [Extending the Runtime](#extending-the-runtime)
65
- - [Web Apps vs React Native](#web-apps-vs-react-native)
66
- - [Publishing to the App Store and Google Play](#publishing-to-the-app-store-and-google-play)
67
- - [Open Source](#open-source)
68
- - [Support](#support)
69
- - [License](#license)
7
+ **[Full documentation](https://setup.despia.com)**
70
8
 
71
9
  ---
72
10
 
@@ -82,920 +20,571 @@ npm install despia-native
82
20
  import despia from 'despia-native';
83
21
  ```
84
22
 
85
- > CDN alternative: `https://cdn.jsdelivr.net/npm/despia-native/+esm` (ESM) or `https://cdn.jsdelivr.net/npm/despia-native/index.min.js` (UMD, global)
86
-
87
- ---
88
-
89
- ## Quick Start
90
-
91
- ```js
92
- import despia from 'despia-native'; // npm / pnpm / yarn only
23
+ Or via CDN:
93
24
 
94
- despia('successhaptic://'); // haptic feedback
95
- const device = await despia('get-uuid://', ['uuid']); // native device ID
96
- console.log(device.uuid);
25
+ ```html
26
+ <!-- UMD -->
27
+ <script src="https://cdn.jsdelivr.net/npm/despia-native/index.min.js"></script>
97
28
  ```
98
29
 
99
- No initialization. No setup. Open your app in the Despia runtime and it works.
100
-
101
- **How it works under the hood**
102
-
103
- 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.
104
-
105
- ```js
106
- // despia('get-uuid://', ['uuid'])
107
- // 1. window.despia = 'get-uuid://' set by SDK
108
- // 2. window.location.href = 'get-uuid://' intercepted by native layer
109
- // 3. window.uuid = '<device-uuid>' written back to WebView
110
- const device = await despia('get-uuid://', ['uuid']);
111
- console.log(device.uuid);
30
+ ```html
31
+ <!-- ESM -->
32
+ <script type="module">
33
+ import despia from 'https://cdn.jsdelivr.net/npm/despia-native/+esm';
34
+ </script>
112
35
  ```
113
36
 
114
- The SDK handles command queuing, promise-based variable watching with a 30-second timeout, and error handling.
115
-
116
37
  ---
117
38
 
118
- ## Environment Detection
119
-
120
- The SDK only activates inside the Despia native runtime. Gate native calls using these checks:
39
+ ## Quick Start
121
40
 
122
41
  ```js
123
- const isDespia = navigator.userAgent.toLowerCase().includes('despia');
124
-
125
- const isDespiaIOS = isDespia &&
126
- (navigator.userAgent.toLowerCase().includes('iphone') ||
127
- navigator.userAgent.toLowerCase().includes('ipad'));
128
-
129
- const isDespiaAndroid = isDespia &&
130
- navigator.userAgent.toLowerCase().includes('android');
131
-
132
- if (isDespia) {
133
- despia('lighthaptic://');
134
- } else {
135
- // Fallback for browser or development environment
136
- }
137
- ```
138
-
139
- ---
140
-
141
- ## AI Agent Rules
142
-
143
- If you are an AI coding agent generating Despia app code, follow these rules:
144
-
145
- - Import as a default: `import despia from 'despia-native'`
146
- - Do not write to `window.despia` directly; it is used internally by the SDK
147
- - Gate all native calls using the environment checks above
148
- - For offline (Local Server) apps, bundle the SDK via a package manager so it is available when the app is served from `http://localhost`
149
- - For Local CDN downloads, do not `await` `localcdn://write` with a watch key; use `window.contentServerChange(item)` or poll with `localcdn://read`
150
- - For the HTTP upload API, do not hardcode localhost ports; use `const host = window.location.host`
151
-
152
- ---
153
-
154
- ## API Reference
155
-
156
- ### `despia(command, watch?)`
157
-
158
-
159
- | Parameter | Type | Description |
160
- | --------- | ---------- | -------------------------------------------------------------- |
161
- | `command` | `string` | A Despia protocol URL, e.g. `'lighthaptic://'` |
162
- | `watch` | `string[]` | Optional. Array of variable names to wait for in the response. |
163
-
164
-
165
- Returns a `Promise` that resolves when all watched variables are set by the native runtime.
166
-
167
- **Timeout behavior**
168
-
169
- 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.
170
-
171
- **Fresh-data behavior**
42
+ import despia from 'despia-native';
172
43
 
173
- Watched variables are cleared before each call to prevent resolving on stale values.
44
+ // Launch a RevenueCat paywall
45
+ despia(`revenuecat://launchPaywall?external_id=${userId}&offering=default`);
174
46
 
175
- - Single variable: `null` is treated as a valid resolved value. Other empty placeholders (`undefined`, `"n/a"`, `{}`, `[]`) are ignored.
176
- - Multiple variables: all variables must be non-null before the Promise resolves.
47
+ // Link the device to your user for OneSignal push
48
+ despia(`setonesignalplayerid://?user_id=${userId}`);
177
49
 
178
- **Direct property access**
50
+ // Store a session token behind Face ID / Touch ID (survives reinstall)
51
+ await despia('setvault://?key=sessionToken&value=abc123&locked=true');
179
52
 
180
- ```js
181
- despia.variableName // Equivalent to window.variableName
53
+ // Read it back later (prompts biometric)
54
+ const { sessionToken } = await despia('readvault://?key=sessionToken', ['sessionToken']);
182
55
  ```
183
56
 
184
- ### Protocol format
185
-
186
- ```
187
- feature://action?param1=value1&param2=value2
188
- ```
57
+ No initialization. Open your app in the Despia runtime and it works.
189
58
 
190
59
  ---
191
60
 
192
- ## Deployment Models
61
+ ## Why Despia
193
62
 
194
- **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.
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.
195
64
 
196
- **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).
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.
197
66
 
198
- **OTA version gating:** Gate features by minimum runtime version using `despia-version-guard`.
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.
199
68
 
200
- ```bash
201
- npm install despia-version-guard
202
- ```
203
-
204
- ```jsx
205
- import { VersionGuard } from 'despia-version-guard';
206
-
207
- const MyApp = () => (
208
- <div>
209
- <StableFeature />
210
- <VersionGuard min_version="21.0.3">
211
- <NewFeature />
212
- </VersionGuard>
213
- </div>
214
- );
215
- ```
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.
216
70
 
217
71
  ---
218
72
 
219
- ## Features
220
-
221
- ### Haptic Feedback
222
-
223
- ```js
224
- despia('lighthaptic://'); // Subtle vibration
225
- despia('heavyhaptic://'); // Strong vibration
226
- despia('successhaptic://'); // Positive confirmation
227
- despia('warninghaptic://'); // Attention alert
228
- despia('errorhaptic://'); // Negative feedback
229
- ```
73
+ ## Contents
230
74
 
231
- [Read Full Documentation](https://setup.despia.com/native-features/haptic-feedback)
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)
232
89
 
233
90
  ---
234
91
 
235
- ### Identity Vault
236
-
237
- 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.
92
+ ## Environment Detection
238
93
 
239
- 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.
94
+ The SDK only activates inside the Despia runtime. Gate native calls:
240
95
 
241
96
  ```js
242
- // Store a JWT token (reading it back requires Face ID / Touch ID)
243
- await despia('setvault://?key=sessionToken&value=abc123&locked=true');
97
+ const isDespia = navigator.userAgent.toLowerCase().includes('despia');
244
98
 
245
- // Read triggers the biometric prompt; token is only returned on success
246
- const data = await despia('readvault://?key=sessionToken', ['sessionToken']);
247
- const token = data.sessionToken;
248
- ```
99
+ const isDespiaIOS = isDespia && (
100
+ navigator.userAgent.toLowerCase().includes('iphone') ||
101
+ navigator.userAgent.toLowerCase().includes('ipad')
102
+ );
249
103
 
250
- If the key does not exist, `readvault://` throws. Wrap in try/catch to handle first-time users.
104
+ const isDespiaAndroid = isDespia && navigator.userAgent.toLowerCase().includes('android');
251
105
 
106
+ if (isDespia) despia('lighthaptic://');
107
+ ```
252
108
 
253
- | Parameter | Description |
254
- | --------- | ---------------------------------------------------------------- |
255
- | `key` | Storage key, e.g. `"userId"` or `"sessionToken"` |
256
- | `value` | String value to store |
257
- | `locked` | `"true"` requires biometrics on read. `"false"` for open access. |
109
+ ---
258
110
 
111
+ ## For AI Coding
259
112
 
260
- **Store a value without biometric protection**
113
+ Full API context: [`llms.txt`](https://setup.despia.com/llms.txt).
261
114
 
262
- ```js
263
- await despia('setvault://?key=userId&value=user123&locked=false');
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
264
123
 
265
- const { userId } = await despia('readvault://?key=userId', ['userId']);
266
- ```
124
+ ---
267
125
 
268
- **Protect a sensitive action with Face ID**
126
+ ## API Reference
269
127
 
270
- ```js
271
- async function confirmWithBiometrics() {
272
- await despia('setvault://?key=confirm&value=yes&locked=true');
273
- try {
274
- const data = await despia('readvault://?key=confirm', ['confirm']);
275
- if (data.confirm === 'yes') {
276
- await performSensitiveAction();
277
- await despia('setvault://?key=confirm&value=&locked=false');
278
- }
279
- } catch {
280
- // User cancelled or biometric failed
281
- }
282
- }
283
- ```
128
+ ### `despia(command, watch?)`
284
129
 
285
- **Prevent free trial abuse**
130
+ | Parameter | Type | Description |
131
+ | --------- | ---------- | ---------------------------------------------- |
132
+ | `command` | `string` | A Despia protocol URL, e.g. `'lighthaptic://'` |
133
+ | `watch` | `string[]` | Variable names to wait for |
286
134
 
287
- ```js
288
- async function checkTrialEligibility() {
289
- try {
290
- const data = await despia('readvault://?key=hasUsedTrial', ['hasUsedTrial']);
291
- return data.hasUsedTrial !== 'yes';
292
- } catch {
293
- // Key not found, first-time user
294
- await despia('setvault://?key=hasUsedTrial&value=yes&locked=false');
295
- return true;
296
- }
297
- }
298
- ```
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`.
299
136
 
300
- [Read Full Documentation](https://setup.despia.com/native-features/storage-vault) + [Read Full Documentation](https://setup.despia.com/native-features/biometrics)
137
+ Protocol format: `feature://action?param1=value1&param2=value2`
301
138
 
302
139
  ---
303
140
 
304
- ### GPS Location
305
-
306
- ```js
307
- // Set up the live update callback
308
- window.onLocationChange = (data) => {
309
- if (!data.active) return;
310
- console.log(data.latitude, data.longitude, data.horizontalAccuracy);
311
- };
141
+ ## Deployment Models
312
142
 
313
- // Start tracking (buffer in seconds, movement threshold in centimetres)
314
- despia('location://?buffer=60&movement=100');
143
+ **Remote hydration (default).** Binary ships without web assets; Despia fetches the current build on launch. Web updates do not require store resubmission.
315
144
 
316
- // Stop tracking and retrieve the session
317
- const { locationSession } = await despia('stoplocation://', ['locationSession']);
318
- ```
145
+ **Local Server.** Build is cached on-device and served from `http://localhost`. See [Local Server](#local-server).
319
146
 
320
- **Server delivery**
147
+ **Version gating.** Use [`despia-version-guard`](https://www.npmjs.com/package/despia-version-guard) to gate features by minimum runtime version:
321
148
 
322
- 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.
149
+ ```jsx
150
+ import { VersionGuard } from 'despia-version-guard';
323
151
 
324
- ```js
325
- despia('location://?server=https://api.example.com/track?user=USER_ID&buffer=30&movement=100');
152
+ <VersionGuard min_version="21.0.3">
153
+ <NewFeature />
154
+ </VersionGuard>
326
155
  ```
327
156
 
328
- Each POST body matches the location object shape returned by `stoplocation://`.
329
-
330
- [Read Full Documentation](https://setup.despia.com/native-features/gps-location)
331
-
332
157
  ---
333
158
 
334
- ### RevenueCat In-App Purchases
159
+ ## Features
335
160
 
336
- **Launch a paywall**
161
+ ### RevenueCat In-App Purchases
337
162
 
338
163
  ```js
164
+ // Paywall
339
165
  despia(`revenuecat://launchPaywall?external_id=${userId}&offering=default`);
340
- ```
341
-
342
-
343
- | Parameter | Required | Description |
344
- | ------------- | -------- | ------------------------------------------------------------------ |
345
- | `external_id` | Yes | Your user ID in RevenueCat |
346
- | `offering` | Yes | RevenueCat offering ID. Use `"default"` for your default offering. |
347
166
 
348
-
349
- **Direct purchase without paywall UI**
350
-
351
- ```js
352
- // iOS
167
+ // Direct purchase
353
168
  despia(`revenuecat://purchase?external_id=${userId}&product=monthly_premium_ios`);
354
-
355
- // Android
356
- despia(`revenuecat://purchase?external_id=${userId}&product=premium:monthly_premium_android`);
357
169
  ```
358
170
 
359
- **Handle purchase success**
360
-
361
- 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.
362
-
363
- 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.
364
-
365
- 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):
366
172
 
367
173
  ```js
368
174
  window.onRevenueCatPurchase = async () => {
369
175
  const { restoredData } = await despia('getpurchasehistory://', ['restoredData']);
370
- const active = (restoredData ?? []).filter(p => p.isActive);
371
-
372
- if (active.some(p => p.entitlementId === 'premium')) unlockPremium();
373
- if (active.some(p => p.entitlementId === 'no_ads')) removeAds();
176
+ if (restoredData.some(p => p.isActive && p.entitlementId === 'premium')) unlockPremium();
374
177
  };
375
178
  ```
376
179
 
377
- **Restore purchases**
378
-
379
- ```js
380
- const { restoredData } = await despia('getpurchasehistory://', ['restoredData']);
381
- const hasPremium = restoredData
382
- .filter(p => p.isActive)
383
- .some(p => p.entitlementId === 'premium');
384
- ```
385
-
386
- 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.
387
-
388
- **Web fallback**
389
-
390
- Neither `launchPaywall` nor `purchase` work in a standard browser. Gate behind the environment check and fall back to a RevenueCat Web Purchase Link:
391
-
392
- ```js
393
- if (isDespia) {
394
- despia(`revenuecat://launchPaywall?external_id=${userId}&offering=default`);
395
- } else {
396
- window.location.href = `https://pay.rev.cat/<your_token>/${encodeURIComponent(userId)}`;
397
- }
398
- ```
180
+ In the browser, fall back to a RevenueCat Web Purchase Link.
399
181
 
400
- [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)
401
183
 
402
184
  ---
403
185
 
404
- ### AppsFlyer Attribution
405
-
406
- 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.
407
-
408
- AppsFlyer must be enabled in **Despia > App > Settings > Integrations > AppsFlyer** with your dev key configured.
409
-
410
- **Attribution variables (injected on every page load)**
411
-
412
- ```js
413
- despia.appsFlyerAttribution // full attribution object (campaign, ad set, UTM params, etc.)
414
- despia.appsFlyerReferrer // normalized source: "tiktok_ad", "facebook_organic", "organic", etc.
415
- despia.appsFlyerUID // unique AppsFlyer user ID
416
- ```
417
-
418
- **User identification (call after login)**
419
-
420
- ```js
421
- despia('appsflyer://set_user_id?customer_user_id=' + encodeURIComponent(userId));
422
- despia('appsflyer://set_email?email=' + encodeURIComponent(email));
423
- despia('appsflyer://set_phone?phone=' + encodeURIComponent(phone)); // hashed with SHA256 automatically
424
- ```
425
-
426
- **GDPR consent**
427
-
428
- ```js
429
- // User accepted
430
- despia('appsflyer://set_consent?is_gdpr=true&has_consent=true');
431
-
432
- // User declined
433
- despia('appsflyer://set_consent?is_gdpr=true&has_consent=false');
434
- ```
435
-
436
- **Log in-app events**
186
+ ### Push Notifications
437
187
 
438
- 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:
439
189
 
440
190
  ```js
441
- // af_purchase maps to Meta Purchase and TikTok CompletePayment
442
- const purchase = { af_revenue: 9.99, af_currency: 'USD', af_content_id: 'pro_plan' };
443
- despia('appsflyer://log_event?event_name=af_purchase&event_values=' + encodeURIComponent(JSON.stringify(purchase)));
191
+ despia(`setonesignalplayerid://?user_id=${userId}`);
444
192
 
445
- // af_complete_registration maps to Meta CompleteRegistration
446
- const reg = { af_registration_method: 'email' };
447
- 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://');
448
196
 
449
- // Custom event (AppsFlyer only)
450
- const onboarding = { step: 1, name: 'select_interests' };
451
- 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');
452
199
  ```
453
200
 
454
- `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.
455
202
 
456
- [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)
457
204
 
458
205
  ---
459
206
 
460
- ### Push Notifications
207
+ ### Identity Vault
461
208
 
462
- 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.
463
210
 
464
211
  ```js
465
- // Link device to your user (call on every authenticated app load)
466
- despia(`setonesignalplayerid://?user_id=${userId}`);
467
-
468
- // Send a local scheduled notification (fires after 60 seconds)
469
- 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');
470
214
 
471
- // Check push permission status
472
- const result = await despia('checkNativePushPermissions://', ['nativePushEnabled']);
473
- if (!result.nativePushEnabled) {
474
- // Direct user to settings to enable notifications
475
- despia('settingsapp://');
476
- }
215
+ // Read (prompts biometric if locked)
216
+ const { sessionToken } = await despia('readvault://?key=sessionToken', ['sessionToken']);
477
217
  ```
478
218
 
479
- **Send to a specific user from your backend**
219
+ `readvault://` throws if the key doesn't exist. Wrap in try/catch.
480
220
 
481
- ```js
482
- await fetch('https://onesignal.com/api/v1/notifications', {
483
- method: 'POST',
484
- headers: {
485
- 'Content-Type': 'application/json',
486
- 'Authorization': 'Basic YOUR_REST_API_KEY',
487
- },
488
- body: JSON.stringify({
489
- app_id: 'ONESIGNAL-APP-ID',
490
- include_external_user_ids: [externalUserId],
491
- headings: { en: title },
492
- contents: { en: message },
493
- data: { url: 'https://yourapp.com/messages/123' } // navigate WebView on tap
494
- }),
495
- });
496
- ```
497
-
498
- 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)
499
222
 
500
223
  ---
501
224
 
502
225
  ### OAuth Authentication
503
226
 
504
- 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.
505
-
506
- 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.
507
228
 
508
229
  ```js
509
- const isDespia = navigator.userAgent.toLowerCase().includes('despia');
510
-
511
230
  const redirectUri = isDespia
512
231
  ? 'https://yourapp.com/native-callback.html'
513
232
  : 'https://yourapp.com/auth/callback';
514
233
 
515
- 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)}`;
516
235
 
517
236
  if (isDespia) {
518
- // Step 1: open a secure native browser session
519
- despia(`oauth://?url=${encodeURIComponent(oauthUrl)}`);
237
+ despia(`oauth://?url=${encodeURIComponent(authUrl)}`);
520
238
  } else {
521
- // Regular web flow
522
- window.location.href = oauthUrl;
239
+ window.location.href = authUrl;
523
240
  }
524
241
  ```
525
242
 
526
- `/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:
527
-
528
- ```js
529
- // Step 2: from inside the callback page, fire the deeplink to return to your app
530
- window.location.href = `{yourscheme}://oauth/auth?access_token=${token}`;
531
- ```
532
-
533
- Despia intercepts the deeplink, closes the browser session, and navigates your WebView to `/auth?access_token=xxx`.
534
-
535
- Deeplink format: `{yourscheme}://oauth/{path}?params`
536
-
537
- Your deeplink scheme is your app name in lowercase with no spaces (e.g. `myapp://`), or a custom Despialink set in the Despia editor.
538
-
539
-
540
- | Deeplink | Result |
541
- | -------------------------------------------- | ------------------------------------------------------------- |
542
- | `{yourscheme}://oauth/auth?access_token=xxx` | Browser closes, WebView navigates to `/auth?access_token=xxx` |
543
- | `{yourscheme}://oauth/home` | Browser closes, WebView navigates to `/home` |
544
- | `{yourscheme}://auth?access_token=xxx` | Browser stays open, user is stuck |
545
-
546
-
547
- **Callback page**
548
-
549
- 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):
550
244
 
551
245
  ```html
552
- <!-- public/native-callback.html -->
553
246
  <script>
554
- var params = new URLSearchParams(window.location.search);
555
- var hash = new URLSearchParams(window.location.hash.substring(1));
556
- var code = params.get('code'); // authorization code flow
557
- var accessToken = hash.get('access_token'); // implicit flow
558
-
559
- if (code) {
560
- // exchange code via your backend, then fire deeplink with tokens
561
- } else if (accessToken) {
562
- window.location.href = '{yourscheme}://oauth/auth?access_token=' + encodeURIComponent(accessToken);
563
- }
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);
564
250
  </script>
565
251
  ```
566
252
 
567
- **Already-mounted `/auth` page**
568
-
569
- 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:
253
+ Deeplink format: `{yourscheme}://oauth/{path}?params`. Without the `oauth/` prefix the browser session stays open.
570
254
 
571
- - React: include `searchParams` in your `useEffect` dependency array
572
- - Vue: use `watch: { '$route.query': { immediate: true, handler } }` instead of reading params in `mounted()`
573
- - Vanilla JS: call your handler on load and add `window.addEventListener('popstate', handler)`
574
-
575
- [Read Full Documentation](https://setup.despia.com/native-features/oauth/introduction)
255
+ [Docs](https://setup.despia.com/native-features/oauth/introduction)
576
256
 
577
257
  ---
578
258
 
579
- ### Clipboard
580
-
581
- ```js
582
- const { clipboarddata } = await despia('getclipboard://', ['clipboarddata']);
583
- ```
259
+ ### AppsFlyer Attribution
584
260
 
585
- [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.
586
262
 
587
- ---
263
+ ```js
264
+ // Attribution available on every launch
265
+ despia.appsFlyerAttribution
266
+ despia.appsFlyerReferrer
267
+ despia.appsFlyerUID
588
268
 
589
- ### Contacts
269
+ // After login
270
+ despia('appsflyer://set_user_id?customer_user_id=' + encodeURIComponent(userId));
590
271
 
591
- ```js
592
- 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)));
593
275
  ```
594
276
 
595
- [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)
596
278
 
597
279
  ---
598
280
 
599
- ### App Information and Device Data
281
+ ### GPS Location
600
282
 
601
283
  ```js
602
- const { versionNumber, bundleNumber } = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
603
- const { uuid } = await despia('get-uuid://', ['uuid']);
604
- const { storeLocation } = await despia('getstorelocation://', ['storeLocation']);
605
- 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']);
606
292
  ```
607
293
 
608
- [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)
609
297
 
610
298
  ---
611
299
 
612
- ### UI Controls and Styling
300
+ ### Apple Health (HealthKit)
301
+
302
+ iOS only. Gate behind `isDespiaIOS`. Permissions are requested on first call per identifier.
613
303
 
614
304
  ```js
615
- despia('spinneron://'); // Show loading spinner
616
- despia('spinneroff://'); // Hide loading spinner
617
- despia('hidebars://on'); // Hide status bar (full screen)
618
- despia('hidebars://off'); // Restore status bar
619
- despia('statusbarcolor://{255, 255, 255}'); // Set status bar background (RGB)
620
- despia('statusbartextcolor://{black}'); // Set status bar text color: black | white
621
- despia('settingsapp://'); // Open native app settings
622
- 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');
623
316
  ```
624
317
 
625
- [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)
626
321
 
627
322
  ---
628
323
 
629
324
  ### File and Media Operations
630
325
 
631
- 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:
632
327
 
633
328
  ```html
634
- <!-- Opens a native action sheet with camera, document scanner, and media library -->
635
- <input type="file">
636
-
637
- <!-- Opens native media gallery for images -->
638
- <input type="file" accept="image/*">
639
-
640
- <!-- Opens native media gallery for video -->
641
- <input type="file" accept="video/*">
642
-
643
- <!-- Opens native camera directly -->
644
- <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 -->
645
333
  ```
646
334
 
647
- Additional file and media commands:
648
-
649
335
  ```js
650
336
  despia('takescreenshot://');
651
337
  despia('savethisimage://?url=https://example.com/image.jpg');
652
338
  despia('file://https://example.com/document.pdf');
653
- despia('shareapp://message?=Check%20out%20this%20app&url=https://myapp.com');
654
- despia('scanningmode://auto'); // auto | on | off
655
- ```
656
-
657
- [Read Full Documentation](https://setup.despia.com/native-features/file-sharing) + [Read Full Documentation](https://setup.despia.com/native-features/camera-roll)
658
-
659
- ---
660
-
661
- ### Apple Health (HealthKit)
662
-
663
- iOS only. Always gate behind `isDespiaIOS`. Despia requests permissions on the first call for each identifier.
664
-
665
- ```js
666
- // Read historical data
667
- const data = await despia('readhealthkit://HKQuantityTypeIdentifierStepCount?days=7', ['healthkitResponse']);
668
- const steps = data.healthkitResponse.HKQuantityTypeIdentifierStepCount;
669
- // [{ date: "2025-11-17", value: 9820, unit: "count" }, ...]
670
-
671
- // Write a value back to HealthKit
672
- despia('writehealthkit://HKQuantityTypeIdentifierBodyMass//74.5');
673
-
674
- // Observe changes and POST to your server whenever HealthKit updates
675
- 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');
676
340
  ```
677
341
 
678
- 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)
679
343
 
680
344
  ---
681
345
 
682
346
  ### AdMob Inline Ads
683
347
 
684
- 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.
685
-
686
- Once enabled, serve ads using standard AdSense, Google Publisher Tag, or IMA for HTML5 tags. No Despia SDK calls needed on the web side.
687
-
688
- ```html
689
- <!-- AdSense banner -->
690
- <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
691
- data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"></script>
692
- <ins class="adsbygoogle"
693
- style="display:block"
694
- data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
695
- data-ad-slot="YYYYYYYYYY"
696
- data-ad-format="auto"
697
- data-full-width-responsive="true"></ins>
698
- <script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
699
- ```
700
-
701
- 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.
702
349
 
703
- 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)
704
351
 
705
352
  ---
706
353
 
707
354
  ### Web Payment Request API
708
355
 
709
- 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.
710
357
 
711
358
  ```js
712
- // Standard Web Payment Request API — works in Despia without any native code changes
713
359
  const request = new PaymentRequest(
714
- [{ supportedMethods: 'https://apple.com/apple-pay' }], // or Google Pay method
715
- {
716
- total: { label: 'Total', amount: { currency: 'USD', value: '9.99' } }
717
- }
360
+ [{ supportedMethods: 'https://apple.com/apple-pay' }],
361
+ { total: { label: 'Total', amount: { currency: 'USD', value: '9.99' } } }
718
362
  );
719
-
720
- const result = await request.show();
721
- await result.complete('success');
363
+ await (await request.show()).complete('success');
722
364
  ```
723
365
 
724
- 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)
725
367
 
726
368
  ---
727
369
 
728
- ### Web Storage APIs
370
+ ### Local Server
729
371
 
730
- 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
+ ```
731
377
 
732
378
  ```js
733
- // localStorage works normally
734
- localStorage.setItem('userId', 'user123');
735
- const userId = localStorage.getItem('userId');
736
-
737
- // IndexedDB works normally
738
- const db = await indexedDB.open('myapp', 1);
739
-
740
- // Web Crypto works normally
741
- const key = await crypto.subtle.generateKey(
742
- { name: 'AES-GCM', length: 256 },
743
- true,
744
- ['encrypt', 'decrypt']
745
- );
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
+ };
746
385
  ```
747
386
 
748
- 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.
749
388
 
750
- [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)
751
390
 
752
391
  ---
753
392
 
754
- ### Local Server
755
-
756
- > **Built-in OTA updates. True offline support. Host with your existing web hosting. No proprietary infrastructure required.**
393
+ ### Local CDN
757
394
 
758
- 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.
759
396
 
760
- 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
+ };
761
402
 
762
- 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`);
763
405
 
764
- ```bash
765
- npm install --save-dev @despia/local
406
+ // Query cache
407
+ const { cdnItems } = await despia('localcdn://query', ['cdnItems']);
766
408
  ```
767
409
 
768
- ```js
769
- // vite.config.js (also available for Webpack, Rollup, Nuxt, SvelteKit, Astro, Remix, esbuild)
770
- import { defineConfig } from 'vite';
771
- 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).
772
411
 
773
- export default defineConfig({
774
- plugins: [
775
- despiaLocalPlugin({
776
- outDir: 'dist',
777
- entryHtml: 'index.html'
778
- })
779
- ]
780
- });
781
- ```
412
+ [Intro](https://setup.despia.com/local-cdn/introduction) · [Reference](https://setup.despia.com/local-cdn/reference)
782
413
 
783
- Or run via CLI after any build:
414
+ ---
784
415
 
785
- ```bash
786
- npx despia-local dist
787
- ```
416
+ ### Web Storage APIs
788
417
 
789
- 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).
790
419
 
791
- ```json
792
- {
793
- "entry": "/index.html",
794
- "deployed_at": "1737225600000",
795
- "assets": [
796
- "/index.html",
797
- "/assets/app.abc123.css",
798
- "/assets/app.def456.js"
799
- ]
800
- }
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
801
430
  ```
802
431
 
803
- [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)
804
433
 
805
434
  ---
806
435
 
807
- ### Local CDN
808
-
809
- 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
810
437
 
811
438
  ```js
812
- // Called by the native runtime when a download completes.
813
- // This is a global function, not an event listener.
814
- window.contentServerChange = (item) => {
815
- // item.local_cdn - localhost URL to use for playback
816
- // item.cdn - original remote URL
817
- // item.index - the uniqueId you passed to the write call
818
- // item.size - file size in bytes
819
- // item.status - "cached" when complete
820
- // item.local_path - absolute path on the device
821
-
822
- console.log('Cached:', item.index, item.local_cdn);
823
- addToDownloadsList(item);
824
- };
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
+ ```
825
448
 
826
- // Fire and forget. Do not await with a watch key; large downloads take longer than the SDK's
827
- // 30-second variable-watch window. Use window.contentServerChange to receive the result instead.
828
- despia(
829
- `localcdn://write?url=${remoteUrl}&filename=videos/clip.mp4&index=clip_1&push=true&pushmessage="Download complete"`
830
- );
449
+ [Safe areas](https://setup.despia.com/native-features/safe-areas) · [App settings](https://setup.despia.com/native-features/app-settings)
831
450
 
832
- // Read cached items by ID
833
- const { cdnItems } = await despia(
834
- `localcdn://read?index=${encodeURIComponent(JSON.stringify(['clip_1']))}`,
835
- ['cdnItems']
836
- );
451
+ ---
837
452
 
838
- // Or query everything in the cache
839
- const { cdnItems: all } = await despia('localcdn://query', ['cdnItems']);
453
+ ### App Information and Device Data
454
+
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']);
840
460
  ```
841
461
 
842
- 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)
843
463
 
844
- ```html
845
- <video src="http://localhost:7777/localcdn/videos/clip.mp4" controls></video>
846
- ```
464
+ ---
847
465
 
848
- **HTTP upload API** (Local Server only)
466
+ ### Clipboard
849
467
 
850
468
  ```js
851
- const host = window.location.host; // Do not hardcode the port; it rotates per session
852
- const fd = new FormData();
853
- fd.append('file', fileInput.files[0]);
854
-
855
- const res = await fetch(`http://${host}/api/upload`, { method: 'POST', body: fd });
856
- const data = await res.json();
857
- // { success: true, fileName: "video.mp4", url: "http://localhost:7777/files/video.mp4" }
469
+ const { clipboarddata } = await despia('getclipboard://', ['clipboarddata']);
858
470
  ```
859
471
 
472
+ [Docs](https://setup.despia.com/native-features/clipboard)
473
+
474
+ ---
860
475
 
861
- | Method | Storage Path | URL Pattern |
862
- | ------------------ | ------------ | -------------------------------------- |
863
- | `localcdn://write` | `/localcdn/` | `localhost:{PORT}/localcdn/{filepath}` |
864
- | `/api/upload` | `/files/` | `localhost:{PORT}/files/{filename}` |
476
+ ### Contacts
865
477
 
478
+ ```js
479
+ const { contacts } = await despia('readcontacts://', ['contacts']);
480
+ ```
866
481
 
867
- [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)
868
483
 
869
484
  ---
870
485
 
871
486
  ## Capability Reference
872
487
 
873
- A full index of native capabilities built into Despia.
874
-
875
- **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.
876
489
 
877
- **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.
878
491
 
879
- **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.
880
493
 
881
- **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.
882
495
 
883
496
  ---
884
497
 
885
498
  ## Safe Area
886
499
 
887
- 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:
888
501
 
889
502
  ```css
890
- .header {
891
- padding-top: var(--safe-area-top);
892
- }
893
-
894
- .footer {
895
- padding-bottom: var(--safe-area-bottom);
896
- }
897
-
898
- .full-height {
899
- height: calc(100vh - var(--safe-area-top) - var(--safe-area-bottom));
900
- }
503
+ .header { padding-top: var(--safe-area-top); }
504
+ .footer { padding-bottom: var(--safe-area-bottom); }
901
505
  ```
902
506
 
903
- Note: left and right safe area variables are not available.
507
+ Left and right insets are not exposed.
904
508
 
905
- [Read Full Documentation](https://setup.despia.com/native-features/safe-areas)
509
+ [Docs](https://setup.despia.com/native-features/safe-areas)
906
510
 
907
511
  ---
908
512
 
909
513
  ## Extending the Runtime
910
514
 
911
- A common concern when adopting any hybrid framework is lock-in: can you escape the custom protocol API later?
912
-
913
- 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.
914
-
915
- **On the bridge architecture**
916
-
917
- 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.
918
-
919
- 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.
920
-
921
- 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.
922
-
923
- **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.
924
516
 
925
517
  ```swift
518
+ // iOS
926
519
  if requestURL.absoluteString.hasPrefix("mycustom://") {
927
- let result = runMyNativeCode()
928
- webView.evaluateJavaScript("window.myResult = '\(result)';")
520
+ webView.evaluateJavaScript("window.myResult = '\(runMyNativeCode())';")
929
521
  decisionHandler(.cancel)
930
522
  return
931
523
  }
932
524
  ```
933
525
 
934
- **Android: MainActivity.java**
935
-
936
526
  ```java
527
+ // Android
937
528
  if (url.startsWith("mycustom://")) {
938
- String result = runMyNativeCode();
939
- webView.evaluateJavascript("window.myResult = '" + result + "';", null);
529
+ webView.evaluateJavascript("window.myResult = '" + runMyNativeCode() + "';", null);
940
530
  return true;
941
531
  }
942
532
  ```
943
533
 
944
- **Call it from your web app**
945
-
946
534
  ```js
947
- import despia from 'despia-native';
948
-
949
- const data = await despia('mycustom://', ['myResult']);
950
- console.log(data.myResult);
535
+ // Web
536
+ const { myResult } = await despia('mycustom://', ['myResult']);
951
537
  ```
952
538
 
953
- 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.
954
540
 
955
- 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
+ ---
956
542
 
957
- 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
958
544
 
959
- > **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.
960
546
 
961
547
  ---
962
548
 
963
- ## Web Apps vs React Native
549
+ ## Publishing
964
550
 
965
- 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.
966
552
 
967
- 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)
968
554
 
969
555
  ---
970
556
 
971
- ## Publishing to the App Store and Google Play
557
+ ## MCP Server
972
558
 
973
- 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.
974
560
 
975
- [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)
976
563
 
977
- ---
564
+ For web builders (Lovable, Bolt, v0), paste: `https://setup.despia.com/mcp`.
978
565
 
979
- ## Open Source
566
+ [Setup guide](https://setup.despia.com/mcp-server)
980
567
 
568
+ ---
981
569
 
982
- | Package | Description | License |
983
- | -------------------------------------------------------------------------- | --------------------- | ------- |
984
- | [despia-native](https://www.npmjs.com/package/despia-native) | JavaScript SDK | MIT |
985
- | [@despia/local](https://www.npmjs.com/package/@despia/local) | Offline asset bundler | MIT |
986
- | [despia-version-guard](https://www.npmjs.com/package/despia-version-guard) | OTA version gating | MIT |
570
+ ## How It Works
987
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:
988
573
 
989
- 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://');
990
577
 
991
- ---
578
+ // the SDK does:
579
+ window.despia = 'lighthaptic://';
580
+ ```
992
581
 
993
- ## 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.
994
583
 
995
- 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.
996
585
 
997
586
  ---
998
587
 
999
- ## License
588
+ ## Support
1000
589
 
1001
- MIT
590
+ [npm@despia.com](mailto:npm@despia.com)