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.
- package/README.md +288 -699
- 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
|
-
|
|
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
|
[](https://www.npmjs.com/package/despia-native)
|
|
6
|
-
[](https://github.com/despia-native/despia-native/blob/main/LICENSE)
|
|
7
6
|
|
|
8
|
-
**[
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
25
|
+
```html
|
|
26
|
+
<!-- UMD -->
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/despia-native/index.min.js"></script>
|
|
97
28
|
```
|
|
98
29
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
// Launch a RevenueCat paywall
|
|
45
|
+
despia(`revenuecat://launchPaywall?external_id=${userId}&offering=default`);
|
|
174
46
|
|
|
175
|
-
|
|
176
|
-
|
|
47
|
+
// Link the device to your user for OneSignal push
|
|
48
|
+
despia(`setonesignalplayerid://?user_id=${userId}`);
|
|
177
49
|
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
|
|
53
|
+
// Read it back later (prompts biometric)
|
|
54
|
+
const { sessionToken } = await despia('readvault://?key=sessionToken', ['sessionToken']);
|
|
182
55
|
```
|
|
183
56
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
```
|
|
187
|
-
feature://action?param1=value1¶m2=value2
|
|
188
|
-
```
|
|
57
|
+
No initialization. Open your app in the Despia runtime and it works.
|
|
189
58
|
|
|
190
59
|
---
|
|
191
60
|
|
|
192
|
-
##
|
|
61
|
+
## Why Despia
|
|
193
62
|
|
|
194
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
94
|
+
The SDK only activates inside the Despia runtime. Gate native calls:
|
|
240
95
|
|
|
241
96
|
```js
|
|
242
|
-
|
|
243
|
-
await despia('setvault://?key=sessionToken&value=abc123&locked=true');
|
|
97
|
+
const isDespia = navigator.userAgent.toLowerCase().includes('despia');
|
|
244
98
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
99
|
+
const isDespiaIOS = isDespia && (
|
|
100
|
+
navigator.userAgent.toLowerCase().includes('iphone') ||
|
|
101
|
+
navigator.userAgent.toLowerCase().includes('ipad')
|
|
102
|
+
);
|
|
249
103
|
|
|
250
|
-
|
|
104
|
+
const isDespiaAndroid = isDespia && navigator.userAgent.toLowerCase().includes('android');
|
|
251
105
|
|
|
106
|
+
if (isDespia) despia('lighthaptic://');
|
|
107
|
+
```
|
|
252
108
|
|
|
253
|
-
|
|
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
|
-
|
|
113
|
+
Full API context: [`llms.txt`](https://setup.despia.com/llms.txt).
|
|
261
114
|
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
266
|
-
```
|
|
124
|
+
---
|
|
267
125
|
|
|
268
|
-
|
|
126
|
+
## API Reference
|
|
269
127
|
|
|
270
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
+
Protocol format: `feature://action?param1=value1¶m2=value2`
|
|
301
138
|
|
|
302
139
|
---
|
|
303
140
|
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
149
|
+
```jsx
|
|
150
|
+
import { VersionGuard } from 'despia-version-guard';
|
|
323
151
|
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
159
|
+
## Features
|
|
335
160
|
|
|
336
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
446
|
-
const
|
|
447
|
-
despia('
|
|
193
|
+
// Check permission
|
|
194
|
+
const { nativePushEnabled } = await despia('checkNativePushPermissions://', ['nativePushEnabled']);
|
|
195
|
+
if (!nativePushEnabled) despia('settingsapp://');
|
|
448
196
|
|
|
449
|
-
//
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
###
|
|
207
|
+
### Identity Vault
|
|
461
208
|
|
|
462
|
-
|
|
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
|
-
//
|
|
466
|
-
despia(
|
|
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
|
-
//
|
|
472
|
-
const
|
|
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
|
-
|
|
219
|
+
`readvault://` throws if the key doesn't exist. Wrap in try/catch.
|
|
480
220
|
|
|
481
|
-
|
|
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
|
-
|
|
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
|
|
234
|
+
const authUrl = `https://provider.com/oauth/authorize?client_id=xxx&redirect_uri=${encodeURIComponent(redirectUri)}`;
|
|
516
235
|
|
|
517
236
|
if (isDespia) {
|
|
518
|
-
|
|
519
|
-
despia(`oauth://?url=${encodeURIComponent(oauthUrl)}`);
|
|
237
|
+
despia(`oauth://?url=${encodeURIComponent(authUrl)}`);
|
|
520
238
|
} else {
|
|
521
|
-
|
|
522
|
-
window.location.href = oauthUrl;
|
|
239
|
+
window.location.href = authUrl;
|
|
523
240
|
}
|
|
524
241
|
```
|
|
525
242
|
|
|
526
|
-
|
|
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
|
|
555
|
-
var
|
|
556
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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
|
-
###
|
|
580
|
-
|
|
581
|
-
```js
|
|
582
|
-
const { clipboarddata } = await despia('getclipboard://', ['clipboarddata']);
|
|
583
|
-
```
|
|
259
|
+
### AppsFlyer Attribution
|
|
584
260
|
|
|
585
|
-
|
|
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
|
-
|
|
269
|
+
// After login
|
|
270
|
+
despia('appsflyer://set_user_id?customer_user_id=' + encodeURIComponent(userId));
|
|
590
271
|
|
|
591
|
-
|
|
592
|
-
const {
|
|
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
|
-
[
|
|
277
|
+
[Intro](https://setup.despia.com/analytics/appsflyer/introduction) · [Attribution](https://setup.despia.com/analytics/appsflyer/attribution)
|
|
596
278
|
|
|
597
279
|
---
|
|
598
280
|
|
|
599
|
-
###
|
|
281
|
+
### GPS Location
|
|
600
282
|
|
|
601
283
|
```js
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
despia('
|
|
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
|
-
|
|
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
|
-
|
|
326
|
+
Standard HTML file inputs route to native UI with events delivered back to the input:
|
|
632
327
|
|
|
633
328
|
```html
|
|
634
|
-
<!--
|
|
635
|
-
<input type="file">
|
|
636
|
-
|
|
637
|
-
|
|
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%
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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' }],
|
|
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
|
-
|
|
366
|
+
[Docs](https://setup.despia.com/native-features/external-links)
|
|
725
367
|
|
|
726
368
|
---
|
|
727
369
|
|
|
728
|
-
###
|
|
370
|
+
### Local Server
|
|
729
371
|
|
|
730
|
-
|
|
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
|
-
//
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
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
|
-
|
|
387
|
+
Or run `npx despia-local dist` after any build. Deploy as normal.
|
|
749
388
|
|
|
750
|
-
[
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
765
|
-
|
|
406
|
+
// Query cache
|
|
407
|
+
const { cdnItems } = await despia('localcdn://query', ['cdnItems']);
|
|
766
408
|
```
|
|
767
409
|
|
|
768
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
+
---
|
|
784
415
|
|
|
785
|
-
|
|
786
|
-
npx despia-local dist
|
|
787
|
-
```
|
|
416
|
+
### Web Storage APIs
|
|
788
417
|
|
|
789
|
-
The
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
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
|
-
[
|
|
432
|
+
[Docs](https://setup.despia.com/native-features/haptic-feedback)
|
|
804
433
|
|
|
805
434
|
---
|
|
806
435
|
|
|
807
|
-
###
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
//
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
|
|
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
|
-
|
|
833
|
-
const { cdnItems } = await despia(
|
|
834
|
-
`localcdn://read?index=${encodeURIComponent(JSON.stringify(['clip_1']))}`,
|
|
835
|
-
['cdnItems']
|
|
836
|
-
);
|
|
451
|
+
---
|
|
837
452
|
|
|
838
|
-
|
|
839
|
-
|
|
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
|
-
|
|
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
|
-
|
|
845
|
-
<video src="http://localhost:7777/localcdn/videos/clip.mp4" controls></video>
|
|
846
|
-
```
|
|
464
|
+
---
|
|
847
465
|
|
|
848
|
-
|
|
466
|
+
### Clipboard
|
|
849
467
|
|
|
850
468
|
```js
|
|
851
|
-
const
|
|
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
|
-
|
|
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
|
-
[
|
|
482
|
+
[Docs](https://setup.despia.com/native-features/contact-access)
|
|
868
483
|
|
|
869
484
|
---
|
|
870
485
|
|
|
871
486
|
## Capability Reference
|
|
872
487
|
|
|
873
|
-
|
|
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
|
-
**
|
|
490
|
+
**SDK bridges.** RevenueCat, OneSignal, AppsFlyer, AdMob, HealthKit.
|
|
878
491
|
|
|
879
|
-
**Infrastructure
|
|
492
|
+
**Infrastructure.** ATT compliance, vendor ID, store location, jailbreak detection, App Clips, Share Extensions, Home Widget configuration.
|
|
880
493
|
|
|
881
|
-
**
|
|
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
|
-
|
|
500
|
+
Top and bottom safe area insets are exposed as CSS custom properties:
|
|
888
501
|
|
|
889
502
|
```css
|
|
890
|
-
.header {
|
|
891
|
-
|
|
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
|
-
|
|
507
|
+
Left and right insets are not exposed.
|
|
904
508
|
|
|
905
|
-
[
|
|
509
|
+
[Docs](https://setup.despia.com/native-features/safe-areas)
|
|
906
510
|
|
|
907
511
|
---
|
|
908
512
|
|
|
909
513
|
## Extending the Runtime
|
|
910
514
|
|
|
911
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
539
|
+
Full Xcode and Android Studio project export is available for custom native code.
|
|
954
540
|
|
|
955
|
-
|
|
541
|
+
---
|
|
956
542
|
|
|
957
|
-
|
|
543
|
+
## Web Native vs React Native
|
|
958
544
|
|
|
959
|
-
|
|
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
|
-
##
|
|
549
|
+
## Publishing
|
|
964
550
|
|
|
965
|
-
|
|
551
|
+
iOS and Android store deployment runs from the web editor. Cloud Mac Mini infrastructure handles code signing, provisioning, and submission.
|
|
966
552
|
|
|
967
|
-
|
|
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
|
-
##
|
|
557
|
+
## MCP Server
|
|
972
558
|
|
|
973
|
-
|
|
559
|
+
Point your AI assistant at the Despia MCP for the full `despia-native` API.
|
|
974
560
|
|
|
975
|
-
[
|
|
561
|
+
[](https://despia.com/mcp/cursor)
|
|
562
|
+
[](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
|
-
|
|
566
|
+
[Setup guide](https://setup.despia.com/mcp-server)
|
|
980
567
|
|
|
568
|
+
---
|
|
981
569
|
|
|
982
|
-
|
|
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
|
-
|
|
574
|
+
```js
|
|
575
|
+
// When you call:
|
|
576
|
+
despia('lighthaptic://');
|
|
990
577
|
|
|
991
|
-
|
|
578
|
+
// the SDK does:
|
|
579
|
+
window.despia = 'lighthaptic://';
|
|
580
|
+
```
|
|
992
581
|
|
|
993
|
-
|
|
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
|
-
|
|
584
|
+
No dependencies, no initialization, no lifecycle to manage.
|
|
996
585
|
|
|
997
586
|
---
|
|
998
587
|
|
|
999
|
-
##
|
|
588
|
+
## Support
|
|
1000
589
|
|
|
1001
|
-
|
|
590
|
+
[npm@despia.com](mailto:npm@despia.com)
|