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