despia-native 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +663 -1010
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,1326 +1,979 @@
1
- # Despia SDK
1
+ # Despia Native
2
2
 
3
- JavaScript SDK for [Despia](https://despia.com) - Add real native device features to your React web app, Vue app, Angular app, or any web framework. Transform your web app into a native iOS & Android app without writing Swift or Kotlin. This npm package provides command queuing and variable watching for seamless integration with Despia's GPU-accelerated native runtime, enabling access to 25+ device APIs through simple JavaScript calls.
3
+ JavaScript SDK for [Despia](https://despia.com). Build with any web framework, access 50+ native device capabilities through a single JavaScript function, and publish to iOS and Android from a browser. No Swift, no Kotlin, no terminal.
4
+
5
+ [npm](https://www.npmjs.com/package/despia-native)
6
+ [license](LICENSE)
7
+
8
+ **[Documentation](https://setup.despia.com)** | **[AI Agent Index](https://setup.despia.com/llms.txt)** | **[iOS Deployment](https://setup.despia.com/deployment/apple-ios/automatic)** | **[Android Deployment](https://setup.despia.com/deployment/google-android/automatic)**
9
+
10
+ Despia is the next generation of web-native development. Unlike earlier hybrid frameworks that routed files through JavaScript, forced Base64 encoding, imposed storage quotas, and ran on insecure `file://` origins, Despia runs your app on a real secure origin (`http://localhost` via Local Server, or your remote URL). Standard web APIs work without restrictions. File operations bypass JavaScript entirely and go directly to the native file system. A 500MB video file uses roughly 100 bytes of JS heap rather than 1.6GB.
11
+
12
+ The runtime runs on WKWebView (iOS) and the Chromium-based WebView (Android) with hardware-accelerated compositing, JIT-compiled JavaScript, automatic DOM optimization, smart caching, and GPU-accelerated WebGL and Canvas 2D. CSS animations run on the compositor thread independently of JavaScript. The runtime has been actively maintained by the same core team since 2011, when it shipped as Advanced WebView. It has powered over 7,500 apps and became Despia in 2023. Native capabilities are implemented in Swift and Java and called from JavaScript through a single typed function. See the [changelog](https://setup.despia.com/changelog) for the full history.
4
13
 
5
14
  ---
6
15
 
7
- ## Quick Start
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==)
21
+ [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)
22
+
23
+ ```
24
+ https://setup.despia.com/mcp
25
+ ```
26
+
27
+ Look for "Add MCP", "MCP Settings", or "Personal Connectors" in your builder. Requires Node.js v18+ for local tools.
28
+
29
+ ---
30
+
31
+ ## Table of Contents
32
+
33
+ - [MCP Server](#mcp-server)
34
+ - [Installation](#installation)
35
+ - [Quick Start](#quick-start)
36
+ - [Environment Detection](#environment-detection)
37
+ - [AI Agent Rules](#ai-agent-rules)
38
+ - [API Reference](#api-reference)
39
+ - [Deployment Models](#deployment-models)
40
+ - [Features](#features)
41
+ - [Haptic Feedback](#haptic-feedback)
42
+ - [Identity Vault](#identity-vault)
43
+ - [GPS Location](#gps-location)
44
+ - [RevenueCat In-App Purchases](#revenuecat-in-app-purchases)
45
+ - [AppsFlyer Attribution](#appsflyer-attribution)
46
+ - [Push Notifications](#push-notifications)
47
+ - [OAuth Authentication](#oauth-authentication)
48
+ - [Clipboard](#clipboard)
49
+ - [Contacts](#contacts)
50
+ - [App Information and Device Data](#app-information-and-device-data)
51
+ - [UI Controls and Styling](#ui-controls-and-styling)
52
+ - [File and Media Operations](#file-and-media-operations)
53
+ - [Apple Health (HealthKit)](#apple-health-healthkit)
54
+ - [AdMob Inline Ads](#admob-inline-ads)
55
+ - [Web Payment Request API](#web-payment-request-api)
56
+ - [Web Storage APIs](#web-storage-apis)
57
+ - [Local CDN](#local-cdn)
58
+ - [Local Server](#local-server)
59
+ - [Capability Reference](#capability-reference)
60
+ - [Safe Area](#safe-area)
61
+ - [Extending the Runtime](#extending-the-runtime)
62
+ - [Web Apps vs React Native](#web-apps-vs-react-native)
63
+ - [Publishing to the App Store and Google Play](#publishing-to-the-app-store-and-google-play)
64
+ - [Open Source](#open-source)
65
+ - [Support](#support)
66
+ - [License](#license)
67
+
68
+ ---
69
+
70
+ ## Installation
8
71
 
9
- **Install the SDK:**
10
72
  ```bash
11
73
  npm install despia-native
74
+ # pnpm add despia-native
75
+ # yarn add despia-native
12
76
  ```
13
77
 
14
- **Use it immediately:**
15
- ```javascript
78
+ ```js
16
79
  import despia from 'despia-native';
17
-
18
- // Simple commands
19
- despia('lighthaptic://');
20
-
21
- // Commands with responses
22
- const appInfo = await despia('getappversion://', ['versionNumber']);
23
80
  ```
24
81
 
25
- **Note:** Always use the real SDK package. Mock implementations will not work on actual devices.
82
+ > CDN alternative: `https://cdn.jsdelivr.net/npm/despia-native/+esm` (ESM) or `https://cdn.jsdelivr.net/npm/despia-native/index.min.js` (UMD, global)
26
83
 
27
84
  ---
28
85
 
29
- **Note: This is for web apps (React, Vue, Angular, etc.) to add native features - NOT for React Native apps.**
86
+ ## Quick Start
30
87
 
31
- ## Web Apps vs React Native
88
+ ```js
89
+ import despia from 'despia-native'; // npm / pnpm / yarn only
32
90
 
33
- **This SDK is for:**
34
- - React web apps (create-react-app, Next.js, Vite, etc.)
35
- - Vue web apps
36
- - Angular web apps
37
- - Svelte web apps
38
- - Any web framework or vanilla JavaScript
39
-
40
- **This SDK is NOT for:**
41
- - React Native apps
42
- - Expo apps
43
- - Native mobile development
44
-
45
- **If you're building a React Native app, this SDK won't work for you.**
46
-
47
- **Import:** `import despia from 'despia-native';` (default export, not destructured)
48
-
49
- **IMPORTANT: This SDK package is REQUIRED for TypeScript, React, Vue, and other modern frameworks!** While `window.despia = ""` works in vanilla JavaScript, this package provides type safety, command queuing, and variable watching for professional development environments.
50
-
51
- ## About Despia
52
-
53
- Despia bridges the gap between web and native mobile development. Build your React web app, Vue app, Angular app, or any web framework using the technologies you already know, then deploy it as a truly native application to the App Store and Google Play - complete with hardware acceleration, offline support, and deep OS integration.
54
-
55
- Our visual editor allows you to configure native widgets, shortcuts, and dynamic app behaviors without touching Xcode or Android Studio. Ship to both app stores with one-click deployment, automatic CI/CD pipelines, and over-the-air updates. Export clean, human-readable Swift and Kotlin source code anytime - you own everything, no vendor lock-in.
56
-
57
- **Go from web app to app store in a weekend - with full native capabilities.**
58
-
59
- ### Key Features:
60
- - **Web Framework Support** - Works with React web apps, Vue web apps, Angular web apps, Svelte web apps, vanilla JS, or any web framework
61
- - **NOT for React Native** - This is for web apps to add native features, not React Native apps
62
- - **Visual Configuration** - Set up native features through an intuitive interface
63
- - **Zero Native Coding** - Access device APIs without writing Swift or Kotlin
64
- - **Source Code Export** - Get complete Xcode and Android Studio projects you can modify
65
- - **True Ownership** - Full access to generated code, no restrictions
66
- - **One-Click Publishing** - Direct deployment to App Store & Google Play
67
- - **Automated Pipeline** - Built-in CI/CD with code signing and provisioning
68
- - **OTA Updates** - Push updates instantly without app store review
69
- - **Performance** - 60fps GPU-accelerated rendering
70
- - **Device API Access** - NFC, HealthKit, Siri, RevenueCat, and 20+ more
71
- - **Single Codebase** - Maintain one web project for iOS, Android, and web
72
-
73
- ### Native Device Features:
74
- - **App Links & Deep Linking** - Universal links and app-to-app communication
75
- - **Widgets & App Clips** - Home screen widgets and instant app experiences
76
- - **In-App Purchases** - RevenueCat integration for subscriptions and payments
77
- - **Push Notifications** - Local and remote notifications with rich media
78
- - **Camera & Media** - Camera access, photo library, and file management
79
- - **Biometric Authentication** - Face ID, Touch ID, and fingerprint
80
- - **Location Services** - Foreground and background GPS tracking
81
- - **Contacts** - Address book access with proper permissions
82
- - **Haptic Feedback** - Tactile responses for user interactions
83
- - **App Analytics** - Version tracking, bundle IDs, device identifiers
84
- - **Screenshots** - Programmatic screen capture
85
- - **Status Bar** - Dynamic styling and visibility control
86
- - **Privacy Controls** - IDFA tracking and permission management
87
- - **Share Sheets** - Native sharing UI
88
- - **UI Controls** - Loading indicators, navigation bars, dynamic layouts
89
- - **Plus 8+ additional native capabilities**
90
-
91
- ## Installation (REQUIRED - NO EXCEPTIONS)
91
+ despia('successhaptic://'); // haptic feedback
92
+ const device = await despia('get-uuid://', ['uuid']); // native device ID
93
+ console.log(device.uuid);
94
+ ```
92
95
 
93
- ```bash
94
- # npm
95
- npm install despia-native
96
+ No initialization. No setup. Open your app in the Despia runtime and it works.
96
97
 
97
- # pnpm
98
- pnpm install despia-native
98
+ **How it works under the hood**
99
99
 
100
- # yarn
101
- yarn add despia-native
100
+ Calling `despia()` sets `window.despia` to the scheme string. On iOS this is intercepted in `WebViewController.swift` via `decidePolicyFor navigationAction`. On Android it is intercepted in `MainActivity.java` via `shouldOverrideUrlLoading`. Results are written back to the WebView as named window variables, which the SDK resolves as a promise.
101
+
102
+ ```js
103
+ // despia('get-uuid://', ['uuid'])
104
+ // 1. window.despia = 'get-uuid://' set by SDK
105
+ // 2. window.location.href = 'get-uuid://' intercepted by native layer
106
+ // 3. window.uuid = '<device-uuid>' written back to WebView
107
+ const device = await despia('get-uuid://', ['uuid']);
108
+ console.log(device.uuid);
102
109
  ```
103
110
 
104
- **This is NOT optional. Your app will NOT work without this package.**
111
+ The SDK handles command queuing, promise-based variable watching with a 30-second timeout, and error handling.
105
112
 
106
- **Package Manager Support:**
107
- - ✅ **npm** - Fully supported
108
- - ✅ **pnpm** - Fully supported (v1.0.19+)
109
- - ✅ **yarn** - Fully supported
113
+ ---
110
114
 
111
- **Do not attempt to:**
112
- - Write your own version
113
- - Copy/paste a mock implementation
114
- - Use window.despia directly in frameworks
115
- - Create a "simplified" version
115
+ ## Environment Detection
116
116
 
117
- **The real SDK is required for ANY Despia functionality to work.**
117
+ The SDK only activates inside the Despia native runtime. Gate native calls using these checks:
118
118
 
119
- ## Important Notes
119
+ ```js
120
+ const isDespia = navigator.userAgent.toLowerCase().includes('despia');
120
121
 
121
- ### Simple Function API
122
- The SDK exports a single function - no initialization or setup required:
122
+ const isDespiaIOS = isDespia &&
123
+ (navigator.userAgent.toLowerCase().includes('iphone') ||
124
+ navigator.userAgent.toLowerCase().includes('ipad'));
123
125
 
124
- ```javascript
125
- import despia from 'despia-native';
126
+ const isDespiaAndroid = isDespia &&
127
+ navigator.userAgent.toLowerCase().includes('android');
126
128
 
127
- // Just call it directly
128
- despia('lighthaptic://');
129
- despia('takescreenshot://');
129
+ if (isDespia) {
130
+ despia('lighthaptic://');
131
+ } else {
132
+ // Fallback for browser or development environment
133
+ }
130
134
  ```
131
135
 
132
- ### Common Mistakes to Avoid
133
- ```javascript
134
- // Don't create mock implementations
135
- const despia = (command) => console.log(command); // Won't work on devices
136
+ ---
136
137
 
137
- // Don't use non-existent methods
138
- despia.ready(); // This method doesn't exist
139
- despia.init(); // This method doesn't exist
140
- despia.setup(); // This method doesn't exist
141
- ```
138
+ ## AI Agent Rules
142
139
 
143
- ## Getting Started
140
+ If you are an AI coding agent generating Despia app code, follow these rules:
144
141
 
145
- ### Step 1: Import Despia SDK
142
+ - Import as a default: `import despia from 'despia-native'`
143
+ - Do not write to `window.despia` directly; it is used internally by the SDK
144
+ - Gate all native calls using the environment checks above
145
+ - For offline (Local Server) apps, bundle the SDK via a package manager so it is available when the app is served from `http://localhost`
146
+ - For Local CDN downloads, do not `await` `localcdn://write` with a watch key; use `window.contentServerChange(item)` or poll with `localcdn://read`
147
+ - For the HTTP upload API, do not hardcode localhost ports; use `const host = window.location.host`
146
148
 
147
- **IMPORTANT: Always import as `despia` (default export), NOT as `{Commands}` or destructured imports!**
149
+ ---
148
150
 
149
- ```javascript
150
- // CORRECT - ES6/ES2015 modules (default import)
151
- import despia from 'despia-native';
151
+ ## API Reference
152
152
 
153
- // CORRECT - CommonJS
154
- const despia = require('despia-native');
153
+ ### `despia(command, watch?)`
155
154
 
156
- // CORRECT - Browser (if using UMD build)
157
- // <script src="despia-native.js"></script>
158
- // despia is available globally
159
155
 
160
- // WRONG - Don't do this!
161
- // import { Commands } from 'despia-native';
162
- // import { despia } from 'despia-native';
163
- ```
156
+ | Parameter | Type | Description |
157
+ | --------- | ---------- | -------------------------------------------------------------- |
158
+ | `command` | `string` | A Despia protocol URL, e.g. `'lighthaptic://'` |
159
+ | `watch` | `string[]` | Optional. Array of variable names to wait for in the response. |
164
160
 
165
- **The SDK exports a single function called `despia` as the default export.**
166
161
 
167
- ### Step 2: Use Native Features (No Setup Required)
162
+ Returns a `Promise` that resolves when all watched variables are set by the native runtime.
168
163
 
169
- ```javascript
170
- // That's it! No initialization needed. Just call despia() directly:
164
+ **Timeout behavior**
171
165
 
172
- // Simple commands (no response needed)
173
- despia('lighthaptic://'); // Light haptic feedback
174
- despia('takescreenshot://'); // Take screenshot
175
- despia('spinneron://'); // Show loading spinner
166
+ 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.
176
167
 
177
- // Commands that return data (use await)
178
- const appInfo = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
179
- console.log(appInfo); // { versionNumber: '1.0.0', bundleNumber: '123' }
168
+ **Fresh-data behavior**
180
169
 
181
- const contacts = await despia('readcontacts://', ['contacts']);
182
- console.log(contacts); // { contacts: [...] }
183
- ```
170
+ Watched variables are cleared before each call to prevent resolving on stale values.
184
171
 
185
- ### Step 3: Handle Responses
172
+ - Single variable: `null` is treated as a valid resolved value. Other empty placeholders (`undefined`, `"n/a"`, `{}`, `[]`) are ignored.
173
+ - Multiple variables: all variables must be non-null before the Promise resolves.
186
174
 
187
- ```javascript
188
- // For commands that set variables, watch for them
189
- const result = await despia('get-uuid://', ['uuid']);
190
- console.log('Device UUID:', result.uuid);
175
+ **Direct property access**
191
176
 
192
- // For commands with no response, just call them
193
- despia('lighthaptic://');
194
- despia('takescreenshot://');
177
+ ```js
178
+ despia.variableName // Equivalent to window.variableName
195
179
  ```
196
180
 
197
- ### Quick Examples
181
+ ### Protocol format
198
182
 
199
- ```javascript
200
- // Haptic feedback
201
- despia('lighthaptic://'); // Light vibration
202
- despia('heavyhaptic://'); // Heavy vibration
203
- despia('successhaptic://'); // Success vibration
183
+ ```
184
+ feature://action?param1=value1&param2=value2
185
+ ```
204
186
 
205
- // App information
206
- const appInfo = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
207
- const deviceId = await despia('get-uuid://', ['uuid']);
187
+ ---
208
188
 
209
- // UI controls
210
- despia('spinneron://'); // Show loading
211
- despia('spinneroff://'); // Hide loading
212
- despia('hidebars://on'); // Hide status bar
189
+ ## Deployment Models
213
190
 
214
- // Screenshots and sharing
215
- despia('takescreenshot://');
216
- despia('shareapp://message?=Hello&url=https://myapp.com');
217
- ```
191
+ **Remote hydration (default):** The binary ships without embedded web assets. On each launch, Despia fetches the current build from your configured hosting URL. Web content updates do not require App Store or Google Play resubmission.
218
192
 
219
- ### Common Import Issues
193
+ **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).
220
194
 
221
- **If you get errors like "Commands is not a function" or "despia is not defined":**
195
+ **OTA version gating:** Gate features by minimum runtime version using `despia-version-guard`.
222
196
 
223
- ```javascript
224
- // WRONG - This will cause errors
225
- import { Commands } from 'despia-native';
226
- import { despia } from 'despia-native';
197
+ ```bash
198
+ npm install despia-version-guard
199
+ ```
227
200
 
228
- // CORRECT - Always use default import
229
- import despia from 'despia-native';
201
+ ```jsx
202
+ import { VersionGuard } from 'despia-version-guard';
230
203
 
231
- // Now you can use it
232
- despia('lighthaptic://');
204
+ const MyApp = () => (
205
+ <div>
206
+ <StableFeature />
207
+ <VersionGuard min_version="21.0.3">
208
+ <NewFeature />
209
+ </VersionGuard>
210
+ </div>
211
+ );
233
212
  ```
234
213
 
235
- **The package exports a single function as the default export, not named exports.**
214
+ ---
236
215
 
237
- ## When to Use This SDK Package
216
+ ## Features
238
217
 
239
- ### Vanilla JavaScript (works without this package)
218
+ ### Haptic Feedback
240
219
 
241
- ```javascript
242
- // This WORKS in vanilla JavaScript:
243
- window.despia = 'lighthaptic://';
244
- window.despia = 'getappversion://';
220
+ ```js
221
+ despia('lighthaptic://'); // Subtle vibration
222
+ despia('heavyhaptic://'); // Strong vibration
223
+ despia('successhaptic://'); // Positive confirmation
224
+ despia('warninghaptic://'); // Attention alert
225
+ despia('errorhaptic://'); // Negative feedback
245
226
  ```
246
227
 
247
- **Vanilla JS is fine for simple cases, but lacks:**
248
- - **TypeScript Support** - No type definitions or autocomplete
249
- - **Command Queuing** - Commands may be lost or executed out of order
250
- - **Variable Watching** - Can't wait for responses from native commands
251
- - **Error Handling** - No timeout or error management
252
- - **Type Safety** - No validation or IntelliSense
228
+ ---
253
229
 
254
- ### Modern Frameworks Need This Package
230
+ ### Identity Vault
255
231
 
256
- ```javascript
257
- // This WON'T work in TypeScript, React, Vue, etc.:
258
- window.despia = 'lighthaptic://'; // TypeScript errors, no type safety
259
- window.despia = 'getappversion://'; // No command queuing, no variable watching
260
- ```
232
+ 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.
261
233
 
262
- ### Modern Frameworks (TypeScript, React, Vue, etc.)
234
+ 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.
263
235
 
264
- ```javascript
265
- // This WORKS perfectly in modern frameworks:
266
- import despia from 'despia-native';
236
+ ```js
237
+ // Store a JWT token (reading it back requires Face ID / Touch ID)
238
+ await despia('setvault://?key=sessionToken&value=abc123&locked=true');
267
239
 
268
- despia('lighthaptic://'); // Type-safe, queued, with error handling
269
- const result = await despia('getappversion://', ['versionNumber']); // Variable watching
240
+ // Read triggers the biometric prompt; token is only returned on success
241
+ const data = await despia('readvault://?key=sessionToken', ['sessionToken']);
242
+ const token = data.sessionToken;
270
243
  ```
271
244
 
272
- **Benefits of using this SDK:**
273
- - **TypeScript Support** - Full type definitions and autocomplete
274
- - **Command Queuing** - Sequential execution, no lost commands
275
- - **Variable Watching** - Automatic waiting for native responses
276
- - **Error Handling** - Timeouts, error management, debugging
277
- - **Type Safety** - Validated commands, autocomplete, IntelliSense
245
+ If the key does not exist, `readvault://` throws. Wrap in try/catch to handle first-time users.
278
246
 
279
- **This package is REQUIRED for TypeScript, React, Vue, Angular, and other modern frameworks.**
280
247
 
281
- ## Development Notes
248
+ | Parameter | Description |
249
+ | --------- | ---------------------------------------------------------------- |
250
+ | `key` | Storage key, e.g. `"userId"` or `"sessionToken"` |
251
+ | `value` | String value to store |
252
+ | `locked` | `"true"` requires biometrics on read. `"false"` for open access. |
282
253
 
283
- ### Environment Detection
284
- The SDK only works within the Despia native runtime. For development and testing:
285
254
 
286
- ```javascript
287
- import despia from 'despia-native';
255
+ **Store a value without biometric protection**
288
256
 
289
- if (navigator.userAgent.includes('despia')) {
290
- // Use native features
291
- despia('lighthaptic://');
292
- } else {
293
- // Handle non-Despia environment
294
- console.log('Running outside Despia runtime');
295
- }
296
- ```
257
+ ```js
258
+ await despia('setvault://?key=userId&value=user123&locked=false');
297
259
 
298
- ### Always Use the Real Package
299
- - Use the real SDK in all environments (development, testing, production)
300
- - Don't create mock implementations - they won't work on actual devices
301
- - The SDK handles missing runtime gracefully
260
+ const { userId } = await despia('readvault://?key=userId', ['userId']);
261
+ ```
302
262
 
303
- ## Handling Non-Despia Environments
263
+ **Protect a sensitive action with Face ID**
304
264
 
305
- The SDK won't work outside the Despia native runtime, but you can detect and handle this:
265
+ ```js
266
+ async function confirmWithBiometrics() {
267
+ await despia('setvault://?key=confirm&value=yes&locked=true');
268
+ try {
269
+ const data = await despia('readvault://?key=confirm', ['confirm']);
270
+ if (data.confirm === 'yes') {
271
+ await performSensitiveAction();
272
+ await despia('setvault://?key=confirm&value=&locked=false');
273
+ }
274
+ } catch {
275
+ // User cancelled or biometric failed
276
+ }
277
+ }
278
+ ```
306
279
 
307
- ```javascript
308
- import despia from 'despia-native';
280
+ **Prevent free trial abuse**
309
281
 
310
- // Check if running in Despia native runtime
311
- if (navigator.userAgent.includes('despia')) {
312
- // Use Despia native features
313
- despia('lighthaptic://');
314
- const appInfo = await despia('getappversion://', ['versionNumber']);
315
- } else {
316
- // Handle non-Despia environment (browser, development, etc.)
317
- console.log('Running outside Despia runtime - native features unavailable');
318
- // Provide fallback behavior or show appropriate message
282
+ ```js
283
+ async function checkTrialEligibility() {
284
+ try {
285
+ const data = await despia('readvault://?key=hasUsedTrial', ['hasUsedTrial']);
286
+ return data.hasUsedTrial !== 'yes';
287
+ } catch {
288
+ // Key not found, first-time user
289
+ await despia('setvault://?key=hasUsedTrial&value=yes&locked=false');
290
+ return true;
291
+ }
319
292
  }
320
293
  ```
321
294
 
322
- **This is the correct way to handle different environments - use the real SDK with proper detection, never mock it.**
323
-
324
- ## Usage
295
+ ---
325
296
 
326
- ### Basic Despia Command Execution
297
+ ### GPS Location
327
298
 
328
- ```javascript
329
- import despia from 'despia-native';
299
+ ```js
300
+ // Set up the live update callback
301
+ window.onLocationChange = (data) => {
302
+ if (!data.active) return;
303
+ console.log(data.latitude, data.longitude, data.horizontalAccuracy);
304
+ };
330
305
 
331
- // Execute a Despia protocol command (no response needed)
332
- despia('lighthaptic://');
306
+ // Start tracking (buffer in seconds, movement threshold in centimetres)
307
+ despia('location://?buffer=60&movement=100');
333
308
 
334
- // Execute command and watch for response variables (await needed)
335
- const result = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
336
- console.log(result); // { versionNumber: '1.0.0', bundleNumber: '123' }
309
+ // Stop tracking and retrieve the session
310
+ const { locationSession } = await despia('stoplocation://', ['locationSession']);
337
311
  ```
338
312
 
339
- ### Despia Command Examples
313
+ **Server delivery**
340
314
 
341
- ```javascript
342
- // Native Widgets
343
- despia('widget://${svg}?refresh=${refresh_time}');
315
+ 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.
344
316
 
345
- // RevenueCat In-App Purchases
346
- despia('revenuecat://purchase?external_id=user_777&product=monthly_premium');
317
+ ```js
318
+ despia('location://?server=https://api.example.com/track?user=USER_ID&buffer=30&movement=100');
319
+ ```
347
320
 
348
- // Restore Purchases
349
- const purchaseData = await despia("getpurchasehistory://", ["restoredData"]);
321
+ Each POST body matches the location object shape returned by `stoplocation://`.
350
322
 
351
- // RevenueCat Paywalls
352
- despia('revenuecat://launchPaywall?external_id=user_777&offering=default');
323
+ Full docs: [https://setup.despia.com/native-features/gps-location](https://setup.despia.com/native-features/gps-location)
353
324
 
354
- // Identity Vault
355
- await despia(`setvault://?key=userId&value=user123&locked=false`);
356
- const vaultData = await despia(`readvault://?key=userId`, ['userId']);
325
+ ---
357
326
 
358
- // OAuth Authentication
359
- despia(`oauth://?url=${encodeURIComponent(oauthUrl)}`);
327
+ ### RevenueCat In-App Purchases
360
328
 
361
- // Local Storage
362
- const encoded = encodeURIComponent(JSON.stringify(userData));
363
- await despia(`writevalue://${encoded}`);
364
- const data = await despia("readvalue://", ["storedValues"]);
329
+ **Launch a paywall**
365
330
 
366
- // Read Clipboard
367
- const clipboardData = await despia('getclipboard://', ['clipboarddata']);
331
+ ```js
332
+ despia(`revenuecat://launchPaywall?external_id=${userId}&offering=default`);
333
+ ```
368
334
 
369
- // Open App Settings
370
- despia("settingsapp://");
371
335
 
372
- // Contact Permissions
373
- despia('requestcontactpermission://');
374
- const contacts = await despia('readcontacts://', ['contacts']);
336
+ | Parameter | Required | Description |
337
+ | ------------- | -------- | ------------------------------------------------------------------ |
338
+ | `external_id` | Yes | Your user ID in RevenueCat |
339
+ | `offering` | Yes | RevenueCat offering ID. Use `"default"` for your default offering. |
375
340
 
376
- // Background Location
377
- despia('backgroundlocationon://');
378
- // Use native browser geolocation API (not despia native runtime)
379
- const watchId = navigator.geolocation.watchPosition(
380
- (position) => console.log('Location:', position),
381
- (error) => console.error('Location error:', error)
382
- );
383
- // To stop
384
- despia('backgroundlocationoff://');
385
- navigator.geolocation.clearWatch(watchId);
386
341
 
387
- // Push Notifications
388
- despia('registerpush://');
389
- despia('sendlocalpushmsg://push.send?s=60&msg=Hello&!#New Message&!#https://myapp.com');
390
- // Set OneSignal external user ID (call on every app load)
391
- despia(`setonesignalplayerid://?user_id=${YOUR_LOGGED_IN_USER_ID}`);
342
+ **Direct purchase without paywall UI**
392
343
 
393
- // Haptic Feedback
394
- despia('lighthaptic://');
395
- despia('heavyhaptic://');
396
- despia('successhaptic://');
397
- despia('warninghaptic://');
398
- despia('errorhaptic://');
344
+ ```js
345
+ // iOS
346
+ despia(`revenuecat://purchase?external_id=${userId}&product=monthly_premium_ios`);
399
347
 
400
- // App Information
401
- const appInfo = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
402
- const deviceInfo = await despia('get-uuid://', ['uuid']);
348
+ // Android
349
+ despia(`revenuecat://purchase?external_id=${userId}&product=premium:monthly_premium_android`);
350
+ ```
403
351
 
404
- // Screenshots and Scanning
405
- despia('takescreenshot://');
406
- despia('scanningmode://auto');
407
- despia('scanningmode://on');
408
- despia('scanningmode://off');
352
+ **Handle purchase success**
409
353
 
410
- // Store and Location
411
- const storeData = await despia('getstorelocation://', ['storeLocation']);
354
+ 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.
412
355
 
413
- // Image and File Operations
414
- despia('savethisimage://?url=${image_url}');
415
- despia('file://${file_url}');
356
+ 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.
416
357
 
417
- // App Control
418
- despia('reset://');
419
- const trackingData = await despia('user-disable-tracking://', ['trackingDisabled']);
358
+ If you have no backend, use `getpurchasehistory://` inside the callback to check entitlements directly from the store:
420
359
 
421
- // UI Controls
422
- despia('spinneron://');
423
- despia('spinneroff://');
424
- despia('hidebars://on');
425
- despia('hidebars://off');
360
+ ```js
361
+ window.onRevenueCatPurchase = async () => {
362
+ const { restoredData } = await despia('getpurchasehistory://', ['restoredData']);
363
+ const active = (restoredData ?? []).filter(p => p.isActive);
426
364
 
427
- // Sharing
428
- despia('shareapp://message?=${message}&url=${url}');
365
+ if (active.some(p => p.entitlementId === 'premium')) unlockPremium();
366
+ if (active.some(p => p.entitlementId === 'no_ads')) removeAds();
367
+ };
368
+ ```
429
369
 
430
- // Status Bar Styling
431
- despia('statusbarcolor://{255, 255, 255}');
432
- despia('statusbartextcolor://{black}');
370
+ **Restore purchases**
433
371
 
434
- // Biometric Authentication
435
- despia('bioauth://');
372
+ ```js
373
+ const { restoredData } = await despia('getpurchasehistory://', ['restoredData']);
374
+ const hasPremium = restoredData
375
+ .filter(p => p.isActive)
376
+ .some(p => p.entitlementId === 'premium');
436
377
  ```
437
378
 
438
- ### Direct Window Variable Access
379
+ 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.
380
+
381
+ **Web fallback**
439
382
 
440
- ```javascript
441
- // Access any window variable directly (useful for Despia response data)
442
- const currentUser = despia.currentUser;
443
- const deviceInfo = despia.deviceInfo;
444
- const appVersion = despia.appVersion;
383
+ Neither `launchPaywall` nor `purchase` work in a standard browser. Gate behind the environment check and fall back to a RevenueCat Web Purchase Link:
384
+
385
+ ```js
386
+ if (isDespia) {
387
+ despia(`revenuecat://launchPaywall?external_id=${userId}&offering=default`);
388
+ } else {
389
+ window.location.href = `https://pay.rev.cat/<your_token>/${encodeURIComponent(userId)}`;
390
+ }
445
391
  ```
446
392
 
447
- ### Advanced Usage with Variable Watching
393
+ Full RevenueCat docs: [https://setup.despia.com/native-features/revenuecat/reference](https://setup.despia.com/native-features/revenuecat/reference)
448
394
 
449
- ```javascript
450
- // Watch multiple response variables
451
- const appData = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
395
+ ---
452
396
 
453
- // Chain multiple Despia commands
454
- despia('lighthaptic://');
455
- const appData2 = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
456
- despia('successhaptic://');
457
- ```
397
+ ### AppsFlyer Attribution
458
398
 
459
- ### Background Location Workflow
399
+ 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.
460
400
 
461
- Background location tracking requires a two-step process:
401
+ AppsFlyer must be enabled in **Despia > App > Settings > Integrations > AppsFlyer** with your dev key configured.
462
402
 
463
- ```javascript
464
- // Step 1: Enable native background location tracking via Despia
465
- despia('backgroundlocationon://');
403
+ **Attribution variables (injected on every page load)**
466
404
 
467
- // Step 2: Use native browser geolocation API for actual tracking (not despia native runtime)
468
- const watchId = navigator.geolocation.watchPosition(
469
- (position) => {
470
- console.log('Location update:', {
471
- latitude: position.coords.latitude,
472
- longitude: position.coords.longitude,
473
- accuracy: position.coords.accuracy,
474
- timestamp: position.timestamp
475
- });
476
- },
477
- (error) => {
478
- console.error('Location error:', error);
479
- },
480
- {
481
- enableHighAccuracy: true,
482
- timeout: 10000,
483
- maximumAge: 60000
484
- }
485
- );
405
+ ```js
406
+ despia.appsFlyerAttribution // full attribution object (campaign, ad set, UTM params, etc.)
407
+ despia.appsFlyerReferrer // normalized source: "tiktok_ad", "facebook_organic", "organic", etc.
408
+ despia.appsFlyerUID // unique AppsFlyer user ID
409
+ ```
410
+
411
+ **User identification (call after login)**
486
412
 
487
- // To stop tracking:
488
- // Step 1: Disable native background tracking via Despia
489
- despia('backgroundlocationoff://');
490
- // Step 2: Clear browser geolocation watch (native API)
491
- navigator.geolocation.clearWatch(watchId);
413
+ ```js
414
+ despia('appsflyer://set_user_id?customer_user_id=' + encodeURIComponent(userId));
415
+ despia('appsflyer://set_email?email=' + encodeURIComponent(email));
416
+ despia('appsflyer://set_phone?phone=' + encodeURIComponent(phone)); // hashed with SHA256 automatically
492
417
  ```
493
418
 
494
- ### Contact Access Workflow
419
+ **GDPR consent**
495
420
 
496
- ```javascript
497
- // Step 1: Request contact permission
498
- despia('requestcontactpermission://');
421
+ ```js
422
+ // User accepted
423
+ despia('appsflyer://set_consent?is_gdpr=true&has_consent=true');
499
424
 
500
- // Step 2: Read contacts after permission granted
501
- const contactData = await despia('readcontacts://', ['contacts']);
502
- console.log('Contacts:', contactData.contacts);
425
+ // User declined
426
+ despia('appsflyer://set_consent?is_gdpr=true&has_consent=false');
503
427
  ```
504
428
 
505
- ### RevenueCat Paywall Workflow
429
+ **Log in-app events**
506
430
 
507
- Create a payment system that uses RevenueCat Paywalls by launching native paywall interfaces configured in your RevenueCat dashboard:
431
+ Standard `af_` prefixed events map automatically to Meta and TikTok conversion events. Custom events appear in AppsFlyer for funnel and retention analysis.
508
432
 
509
- ```javascript
510
- // First, install the package:
511
- // npm install despia-native
433
+ ```js
434
+ // af_purchase maps to Meta Purchase and TikTok CompletePayment
435
+ const purchase = { af_revenue: 9.99, af_currency: 'USD', af_content_id: 'pro_plan' };
436
+ despia('appsflyer://log_event?event_name=af_purchase&event_values=' + encodeURIComponent(JSON.stringify(purchase)));
512
437
 
513
- // Then import it:
514
- import despia from 'despia-native';
515
-
516
- // Launch a paywall for a specific offering
517
- despia('revenuecat://launchPaywall?external_id=USER_ID&offering=default');
438
+ // af_complete_registration maps to Meta CompleteRegistration
439
+ const reg = { af_registration_method: 'email' };
440
+ despia('appsflyer://log_event?event_name=af_complete_registration&event_values=' + encodeURIComponent(JSON.stringify(reg)));
518
441
 
519
- // Example offerings you might configure:
520
- // - "default" - your main offering
521
- // - "premium" - premium tier offering
522
- // - "annual_sale" - special promotional offering
442
+ // Custom event (AppsFlyer only)
443
+ const onboarding = { step: 1, name: 'select_interests' };
444
+ despia('appsflyer://log_event?event_name=onboarding_step&event_values=' + encodeURIComponent(JSON.stringify(onboarding)));
523
445
  ```
524
446
 
525
- **Handling Purchase Success:**
447
+ `event_values` must always be `JSON.stringify()`ed and `encodeURIComponent()`ed. Event names are case-sensitive, lowercase alphanumeric and underscores only, max 300 unique names per day.
526
448
 
527
- The Despia Native Runtime will call the global function `onRevenueCatPurchase()` when an in-app purchase or subscription is successfully made on the client side. Although this should not grant access immediately, it's a good time to start polling your backend to check if the RevenueCat webhook has already updated the user's status or plan permissions.
449
+ Full attribution docs: [https://setup.despia.com/analytics/appsflyer/introduction](https://setup.despia.com/analytics/appsflyer/introduction)
528
450
 
529
- ```javascript
530
- // Define the global callback function
531
- window.onRevenueCatPurchase = function() {
532
- console.log('Purchase successful! Polling backend for status update...');
533
-
534
- // Start polling your backend to check if RevenueCat webhook
535
- // has updated the user's status or plan permissions
536
- // This ensures you don't grant access before the webhook processes
537
- };
538
- ```
451
+ ---
539
452
 
540
- This will launch a native paywall interface configured in your RevenueCat dashboard, handling purchases through Apple App Store and Google Play billing.
453
+ ### Push Notifications
541
454
 
542
- ### Identity Vault Workflow
455
+ Despia requests push permission and registers the device with OneSignal automatically at app launch. Call `setonesignalplayerid://` on every authenticated load to link the device to your user.
543
456
 
544
- The identity vault provides persistent, cross-device storage with optional biometric protection:
457
+ ```js
458
+ // Link device to your user (call on every authenticated app load)
459
+ despia(`setonesignalplayerid://?user_id=${userId}`);
545
460
 
546
- ```javascript
547
- // First, install the package:
548
- // npm install despia-native
461
+ // Send a local scheduled notification (fires after 60 seconds)
462
+ despia('sendlocalpushmsg://push.send?s=60&msg=Hello&!#New Message&!#https://myapp.com');
549
463
 
550
- // Then import it:
551
- import despia from 'despia-native';
464
+ // Check push permission status
465
+ const result = await despia('checkNativePushPermissions://', ['nativePushEnabled']);
466
+ if (!result.nativePushEnabled) {
467
+ // Direct user to settings to enable notifications
468
+ despia('settingsapp://');
469
+ }
470
+ ```
552
471
 
553
- // Store identity data
554
- await despia(`setvault://?key=${keyName}&value=${value}&locked=${isLocked}`);
472
+ **Send to a specific user from your backend**
555
473
 
556
- // Retrieve identity data
557
- const data = await despia(`readvault://?key=${keyName}`, [keyName]);
558
- const value = data[keyName];
474
+ ```js
475
+ await fetch('https://onesignal.com/api/v1/notifications', {
476
+ method: 'POST',
477
+ headers: {
478
+ 'Content-Type': 'application/json',
479
+ 'Authorization': 'Basic YOUR_REST_API_KEY',
480
+ },
481
+ body: JSON.stringify({
482
+ app_id: 'ONESIGNAL-APP-ID',
483
+ include_external_user_ids: [externalUserId],
484
+ headings: { en: title },
485
+ contents: { en: message },
486
+ data: { url: 'https://yourapp.com/messages/123' } // navigate WebView on tap
487
+ }),
488
+ });
559
489
  ```
560
490
 
561
- **Parameters:**
491
+ When configuring OneSignal, select **Native iOS** and **Native Android** as the platforms. For critical alerts that bypass Do Not Disturb, see the full push docs: [https://setup.despia.com](https://setup.despia.com)
562
492
 
563
- - **key** - Name for your stored data (use simple names like "userId", "deviceId", "sessionToken")
564
- - **value** - The data to store (text/string)
565
- - **locked** - Set to `'true'` to require Face ID/fingerprint, `'false'` for normal storage
493
+ ---
566
494
 
567
- **Features:**
495
+ ### OAuth Authentication
568
496
 
569
- - **Persistent storage** - Data survives app restarts, updates, and even uninstall/reinstall
570
- - **Cross-device sync** - Works across all user's devices with the same Apple ID or Google account
571
- - **User tracking** - Identify the same user even after they uninstall and reinstall your app
572
- - **Face ID protection** - Optional biometric lock for sensitive actions
573
- - **Automatic timeout** - 30-second timeout prevents app freezing
497
+ The flow uses two Despia URL protocols. `oauth://` opens a secure browser session (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android). The `{scheme}://oauth/` prefix on the return deeplink tells Despia to close that session and navigate your WebView to the path that follows.
574
498
 
575
- **Perfect for:**
499
+ 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.
576
500
 
577
- - Identifying users across sessions
578
- - Preventing free trial abuse (track device even after uninstall)
579
- - Storing login session tokens
580
- - Protecting sensitive actions with Face ID/Touch ID
581
- - Saving user preferences and app settings
501
+ ```js
502
+ const isDespia = navigator.userAgent.toLowerCase().includes('despia');
582
503
 
583
- **Example Usage:**
504
+ const redirectUri = isDespia
505
+ ? 'https://yourapp.com/native-callback.html'
506
+ : 'https://yourapp.com/auth/callback';
584
507
 
585
- ```javascript
586
- // Store user ID (normal storage)
587
- await despia(`setvault://?key=userId&value=user123&locked=false`);
508
+ const oauthUrl = `https://provider.com/oauth/authorize?client_id=xxx&redirect_uri=${encodeURIComponent(redirectUri)}`;
588
509
 
589
- // Store session token with biometric protection
590
- await despia(`setvault://?key=sessionToken&value=abc123xyz&locked=true`);
510
+ if (isDespia) {
511
+ // Step 1: open a secure native browser session
512
+ despia(`oauth://?url=${encodeURIComponent(oauthUrl)}`);
513
+ } else {
514
+ // Regular web flow
515
+ window.location.href = oauthUrl;
516
+ }
517
+ ```
591
518
 
592
- // Retrieve stored data
593
- const userData = await despia(`readvault://?key=userId`, ['userId']);
594
- console.log('User ID:', userData.userId);
519
+ `/native-callback.html` runs inside the secure browser session. It receives the tokens or authorization code from the provider, handles the exchange if needed, then fires the deeplink to close the session and return to the app:
595
520
 
596
- const sessionData = await despia(`readvault://?key=sessionToken`, ['sessionToken']);
597
- console.log('Session Token:', sessionData.sessionToken);
521
+ ```js
522
+ // Step 2: from inside the callback page, fire the deeplink to return to your app
523
+ window.location.href = `{yourscheme}://oauth/auth?access_token=${token}`;
598
524
  ```
599
525
 
600
- ### OAuth Authentication Workflow
526
+ Despia intercepts the deeplink, closes the browser session, and navigates your WebView to `/auth?access_token=xxx`.
601
527
 
602
- Launch OAuth authentication in a secure native browser session:
528
+ Deeplink format: `{yourscheme}://oauth/{path}?params`
603
529
 
604
- ```javascript
605
- // First, install the package:
606
- // npm install despia-native
530
+ Your deeplink scheme is your app name in lowercase with no spaces (e.g. `myapp://`), or a custom Despialink set in the Despia editor.
607
531
 
608
- // Then import it:
609
- import despia from 'despia-native';
610
532
 
611
- // Launch OAuth flow
612
- const oauthUrl = 'https://your-provider.com/oauth/authorize?client_id=xxx&redirect_uri=xxx';
613
- despia(`oauth://?url=${encodeURIComponent(oauthUrl)}`);
614
- ```
533
+ | Deeplink | Result |
534
+ | -------------------------------------------- | ------------------------------------------------------------- |
535
+ | `{yourscheme}://oauth/auth?access_token=xxx` | Browser closes, WebView navigates to `/auth?access_token=xxx` |
536
+ | `{yourscheme}://oauth/home` | Browser closes, WebView navigates to `/home` |
537
+ | `{yourscheme}://auth?access_token=xxx` | Browser stays open, user is stuck |
615
538
 
616
- **How it works:**
617
539
 
618
- 1. **Opens secure browser session:**
619
- - **iOS**: ASWebAuthenticationSession (secure Safari sheet)
620
- - **Android**: Chrome Custom Tabs (secure Chrome overlay)
540
+ **Callback page**
621
541
 
622
- 2. **User completes OAuth** in the secure browser session
542
+ 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.
623
543
 
624
- 3. **Parse tokens from callback URL** - OAuth providers typically return tokens in the URL hash or query parameters:
625
- ```javascript
626
- // Example: Parse tokens from URL hash (implicit flow)
627
- const hash = window.location.hash.substring(1);
628
- const hashParams = new URLSearchParams(hash);
629
- const accessToken = hashParams.get('access_token');
630
- const refreshToken = hashParams.get('refresh_token');
631
- ```
544
+ ```html
545
+ <!-- public/native-callback.html -->
546
+ <script>
547
+ var params = new URLSearchParams(window.location.search);
548
+ var hash = new URLSearchParams(window.location.hash.substring(1));
549
+ var code = params.get('code'); // authorization code flow
550
+ var accessToken = hash.get('access_token'); // implicit flow
632
551
 
633
- 4. **Close browser and return to app** - Use your app's deeplink scheme with the `oauth/` prefix:
634
- ```javascript
635
- // From your callback page (still in secure browser session)
636
- window.location.href = `myapp://oauth/auth?access_token=${accessToken}&refresh_token=${refreshToken}`;
637
- ```
552
+ if (code) {
553
+ // exchange code via your backend, then fire deeplink with tokens
554
+ } else if (accessToken) {
555
+ window.location.href = '{yourscheme}://oauth/auth?access_token=' + encodeURIComponent(accessToken);
556
+ }
557
+ </script>
558
+ ```
559
+
560
+ **Already-mounted `/auth` page**
638
561
 
639
- 5. **Handle deeplink in app** - The native app intercepts the deeplink, closes the browser session, and navigates your WebView to the specified path with query parameters.
562
+ 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:
640
563
 
641
- **Deeplink format:** `{scheme}://oauth/{path}?params`
564
+ - React: include `searchParams` in your `useEffect` dependency array
565
+ - Vue: use `watch: { '$route.query': { immediate: true, handler } }` instead of reading params in `mounted()`
566
+ - Vanilla JS: call your handler on load and add `window.addEventListener('popstate', handler)`
642
567
 
643
- - `myapp://` - Your app's deeplink scheme
644
- - `oauth/` - Required prefix that tells native code to close the browser session
645
- - `{path}` - Where to navigate in your app (e.g., `auth`, `home`, `profile`)
646
- - `?params` - Query parameters passed to that page
568
+ Full docs: [https://setup.despia.com/native-features/o-auth-2-0](https://setup.despia.com/native-features/o-auth-2-0)
647
569
 
648
- **Examples:**
649
- - `myapp://oauth/auth?access_token=xxx` - Closes browser, opens `/auth?access_token=xxx`
650
- - `myapp://oauth/home` - Closes browser, opens `/home`
651
- - `myapp://oauth/profile?tab=settings` - Closes browser, opens `/profile?tab=settings`
570
+ ---
652
571
 
653
- **Perfect for:**
654
- - Google, Facebook, Apple, GitHub, and other OAuth providers
655
- - Secure authentication flows without leaving your app
656
- - Native browser sessions with automatic cleanup
572
+ ### Clipboard
657
573
 
658
- ### Local Storage Workflow
574
+ ```js
575
+ const { clipboarddata } = await despia('getclipboard://', ['clipboarddata']);
576
+ ```
659
577
 
660
- Create a local storage system with cross-platform support (data is cleared on app uninstall):
578
+ ---
661
579
 
662
- ```javascript
663
- // First, install the package:
664
- // npm install despia-native
580
+ ### Contacts
665
581
 
666
- // Then import it:
667
- import despia from 'despia-native';
582
+ ```js
583
+ const { contacts } = await despia('readcontacts://', ['contacts']);
584
+ ```
668
585
 
669
- // This SDK is compatible only with the Native Despia Runtime.
670
- // Ensure that the User Agent string includes "despia" before running this code.
671
- // If the User Agent string doesn't include "despia" you can use Local Storage as a web fallback.
586
+ ---
672
587
 
673
- // Save data
674
- const userData = { refresh_token: "SSBMT1ZFIERFU1BJQSBOQVRJVkUgU08gTVVDSCBJIFdBTk5BIEtJU1MgSVQh" };
675
- const encoded = encodeURIComponent(JSON.stringify(userData));
676
- await despia(`writevalue://${encoded}`);
588
+ ### App Information and Device Data
677
589
 
678
- // Retrieve data
679
- const data = await despia("readvalue://", ["storedValues"]);
680
- const userData = JSON.parse(decodeURIComponent(data.storedValues));
681
- console.log(userData.refresh_token);
590
+ ```js
591
+ const { versionNumber, bundleNumber } = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
592
+ const { uuid } = await despia('get-uuid://', ['uuid']);
593
+ const { storeLocation } = await despia('getstorelocation://', ['storeLocation']);
594
+ const { trackingDisabled } = await despia('user-disable-tracking://', ['trackingDisabled']);
682
595
  ```
683
596
 
684
- **How it works:**
685
-
686
- - **Save data**: Use `writevalue://` with a JSON-encoded string to store data on the device
687
- - **Retrieve data**: Use `readvalue://` with `["storedValues"]` to get the stored data
688
- - **Data format**: Data is stored as a single string and returned in the response object
689
- - **Cross-platform**: Works on both iOS and Android
690
- - **Lifecycle**: Data persists across app restarts but is cleared on app uninstall
597
+ ---
691
598
 
692
- **Important Notes:**
599
+ ### UI Controls and Styling
693
600
 
694
- - **Runtime compatibility**: Only works with Despia Native Runtime (check User Agent for "despia")
695
- - **Web fallback**: Use Local Storage as a fallback if not running in Despia Native Runtime
696
- - **UI blocking**: Refrain from blocking any UI elements or adding loading screens before data is loaded, as most sessions will not have initial data yet if no data has been stored
601
+ ```js
602
+ despia('spinneron://'); // Show loading spinner
603
+ despia('spinneroff://'); // Hide loading spinner
604
+ despia('hidebars://on'); // Hide status bar (full screen)
605
+ despia('hidebars://off'); // Restore status bar
606
+ despia('statusbarcolor://{255, 255, 255}'); // Set status bar background (RGB)
607
+ despia('statusbartextcolor://{black}'); // Set status bar text color: black | white
608
+ despia('settingsapp://'); // Open native app settings
609
+ despia('reset://'); // Reset the app
610
+ ```
697
611
 
698
- **Perfect for:**
612
+ ---
699
613
 
700
- - Storing user preferences and settings
701
- - Caching temporary data
702
- - Storing session tokens (non-sensitive)
703
- - App configuration data
614
+ ### File and Media Operations
704
615
 
705
- ### Restore Purchases Workflow
616
+ Despia automatically intercepts standard HTML file inputs and routes them to native UI. No Base64 blobs, no memory issues. File events are injected back into the standard HTML file input.
706
617
 
707
- Retrieve purchase history from the native app stores to implement "Restore Purchases" functionality and verify user entitlements:
618
+ ```html
619
+ <!-- Opens a native action sheet with camera, document scanner, and media library -->
620
+ <input type="file">
708
621
 
709
- ```javascript
710
- // First, install the package:
711
- // npm install despia-native
622
+ <!-- Opens native media gallery for images -->
623
+ <input type="file" accept="image/*">
712
624
 
713
- // Then import it:
714
- import despia from 'despia-native';
625
+ <!-- Opens native media gallery for video -->
626
+ <input type="file" accept="video/*">
715
627
 
716
- // Retrieve purchase history
717
- const data = await despia("getpurchasehistory://", ["restoredData"]);
718
- const purchases = data.restoredData;
719
- console.log(purchases);
628
+ <!-- Opens native camera directly -->
629
+ <input type="file" accept="image/*" capture="environment">
720
630
  ```
721
631
 
722
- **How it works:**
723
-
724
- Despia queries the native platform's billing system to retrieve all purchases associated with the current user's App Store or Google Play account. This includes active subscriptions, expired subscriptions, consumables, and non-consumable (lifetime) purchases. The data is normalized into a consistent format across both iOS and Android platforms.
725
-
726
- **Response Structure:**
727
-
728
- Each purchase object in the response array includes:
729
-
730
- - **transactionId** - Unique identifier for this specific transaction
731
- - **originalTransactionId** - Identifier linking to the original purchase (useful for subscription renewals)
732
- - **productId** - The product identifier configured in App Store Connect / Google Play Console
733
- - **type** - Either `"subscription"` or `"product"` (one-time purchase)
734
- - **entitlementId** - The entitlement/access level this purchase grants
735
- - **externalUserId** - External user identifier if configured
736
- - **isAnonymous** - Boolean indicating if the purchase is anonymous
737
- - **isActive** - Boolean indicating if the purchase currently grants access
738
- - **willRenew** - Boolean indicating if a subscription will auto-renew
739
- - **purchaseDate** - ISO timestamp of the most recent transaction
740
- - **originalPurchaseDate** - ISO timestamp of the initial purchase
741
- - **expirationDate** - ISO timestamp when access expires (`null` for lifetime purchases)
742
- - **store** - Either `"app_store"` or `"play_store"`
743
- - **country** - User's country code
744
- - **environment** - `"production"` or `"sandbox"`
745
- - **receipt** - The raw receipt data for server-side validation
746
-
747
- **Example Response (iOS):**
748
-
749
- ```javascript
750
- [
751
- {
752
- "transactionId": "1000000987654321",
753
- "originalTransactionId": "1000000123456789",
754
- "productId": "com.app.premium.monthly",
755
- "type": "subscription",
756
- "entitlementId": "premium",
757
- "externalUserId": "abc123",
758
- "isAnonymous": false,
759
- "isActive": true,
760
- "willRenew": true,
761
- "purchaseDate": "2024-01-15T14:32:05Z",
762
- "originalPurchaseDate": "2023-06-20T09:15:33Z",
763
- "expirationDate": "2024-02-15T14:32:05Z",
764
- "store": "app_store",
765
- "country": "USA",
766
- "receipt": "MIIbngYJKoZIhvcNAQcCoIIbajCCG2YCAQExDzAN...",
767
- "environment": "production"
768
- },
769
- {
770
- "transactionId": "1000000555555555",
771
- "originalTransactionId": "1000000555555555",
772
- "productId": "com.app.removeads",
773
- "type": "product",
774
- "entitlementId": "no_ads",
775
- "externalUserId": "abc123",
776
- "isAnonymous": false,
777
- "isActive": true,
778
- "willRenew": false,
779
- "purchaseDate": "2023-12-01T08:00:00Z",
780
- "originalPurchaseDate": "2023-12-01T08:00:00Z",
781
- "expirationDate": null,
782
- "store": "app_store",
783
- "country": "USA",
784
- "receipt": "MIIbngYJKoZIhvcNAQcCoIIbajCCG2YCAQExDzAN...",
785
- "environment": "production"
786
- }
787
- ]
788
- ```
632
+ Additional file and media commands:
789
633
 
790
- **Example Response (Android):**
791
-
792
- ```javascript
793
- [
794
- {
795
- "transactionId": "GPA.3372-4150-9088-12345",
796
- "originalTransactionId": "GPA.3372-4150-9088-12345",
797
- "productId": "com.app.premium.monthly",
798
- "type": "subscription",
799
- "entitlementId": "premium",
800
- "externalUserId": "abc123",
801
- "isAnonymous": false,
802
- "isActive": true,
803
- "willRenew": true,
804
- "purchaseDate": "2024-01-15T14:32:05Z",
805
- "originalPurchaseDate": "2023-06-20T09:15:33Z",
806
- "expirationDate": "2024-02-15T14:32:05Z",
807
- "store": "play_store",
808
- "country": "US",
809
- "receipt": "kefhajglhaljhfajkfajk.AO-J1OxBnT3hAjkl5FjpKc9...",
810
- "environment": "production"
811
- },
812
- {
813
- "transactionId": "GPA.3372-4150-9088-67890",
814
- "originalTransactionId": "GPA.3372-4150-9088-67890",
815
- "productId": "com.app.removeads",
816
- "type": "product",
817
- "entitlementId": "no_ads",
818
- "externalUserId": "abc123",
819
- "isAnonymous": false,
820
- "isActive": true,
821
- "willRenew": false,
822
- "purchaseDate": "2023-12-01T08:00:00Z",
823
- "originalPurchaseDate": "2023-12-01T08:00:00Z",
824
- "expirationDate": null,
825
- "store": "play_store",
826
- "country": "US",
827
- "receipt": "minodkpfokbofclncmaa.AO-J1Oy2fXpTml7rKxE3vNc9...",
828
- "environment": "production"
829
- }
830
- ]
634
+ ```js
635
+ despia('takescreenshot://');
636
+ despia('savethisimage://?url=https://example.com/image.jpg');
637
+ despia('file://https://example.com/document.pdf');
638
+ despia('shareapp://message?=Check%20out%20this%20app&url=https://myapp.com');
639
+ despia('scanningmode://auto'); // auto | on | off
831
640
  ```
832
641
 
833
- **Check Active Entitlements:**
642
+ ---
834
643
 
835
- ```javascript
836
- const data = await despia("getpurchasehistory://", ["restoredData"]);
837
- const purchases = data.restoredData;
644
+ ### Apple Health (HealthKit)
838
645
 
839
- // Filter for active purchases only
840
- const activePurchases = purchases.filter(p => p.isActive);
646
+ iOS only. Always gate behind `isDespiaIOS`. Despia requests permissions on the first call for each identifier.
841
647
 
842
- // Check if user has premium access
843
- const hasPremium = activePurchases.some(p => p.entitlementId === "premium");
648
+ ```js
649
+ // Read historical data
650
+ const data = await despia('readhealthkit://HKQuantityTypeIdentifierStepCount?days=7', ['healthkitResponse']);
651
+ const steps = data.healthkitResponse.HKQuantityTypeIdentifierStepCount;
652
+ // [{ date: "2025-11-17", value: 9820, unit: "count" }, ...]
844
653
 
845
- if (hasPremium) {
846
- // Grant premium features
847
- }
654
+ // Write a value back to HealthKit
655
+ despia('writehealthkit://HKQuantityTypeIdentifierBodyMass//74.5');
656
+
657
+ // Observe changes and POST to your server whenever HealthKit updates
658
+ despia('healthkit://observe?types=HKQuantityTypeIdentifierStepCount&frequency=hourly&server=https://api.example.com/webhook?user=USER_ID');
848
659
  ```
849
660
 
850
- **Perfect for:**
661
+ Pass any valid `HKQuantityTypeIdentifier`, `HKCategoryTypeIdentifier`, `HKWorkoutTypeIdentifier`, or `HKCharacteristicTypeIdentifier` directly. Sleep, workouts, heart rate, body mass, and all other HealthKit types are supported. Full docs: [https://setup.despia.com](https://setup.despia.com)
851
662
 
852
- - Implementing "Restore Purchases" buttons required by App Store guidelines
853
- - Verifying user entitlements on app launch
854
- - Checking subscription status and renewal information
855
- - Server-side receipt validation using raw receipt data
856
- - Cross-platform purchase history retrieval
663
+ ---
857
664
 
858
- **Important Notes:**
665
+ ### AdMob Inline Ads
859
666
 
860
- - This feature requires native capabilities which are fully provided by the `despia-native` npm package
861
- - No additional native libraries are needed
862
- - Please follow the installation instructions for the `despia-native` npm package closely
863
- - Implementation as mentioned is critical for App Store compliance
667
+ Despia implements Google's WebView API for Ads on both platforms, connecting the Mobile Ads SDK directly to the WebView so AdMob ads render as real DOM elements inside your web UI, not as native overlays. Enabled from **Despia > App > Settings > AdMob**. Requires a rebuild after enabling.
864
668
 
865
- ### Read Clipboard Workflow
669
+ Once enabled, serve ads using standard AdSense, Google Publisher Tag, or IMA for HTML5 tags. No Despia SDK calls needed on the web side.
866
670
 
867
- Read clipboard data from the device:
671
+ ```html
672
+ <!-- AdSense banner -->
673
+ <script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
674
+ data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"></script>
675
+ <ins class="adsbygoogle"
676
+ style="display:block"
677
+ data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
678
+ data-ad-slot="YYYYYYYYYY"
679
+ data-ad-format="auto"
680
+ data-full-width-responsive="true"></ins>
681
+ <script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
682
+ ```
868
683
 
869
- ```javascript
870
- // First, install the package:
871
- // npm install despia-native
684
+ 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.
872
685
 
873
- // Then import it:
874
- import despia from 'despia-native';
686
+ Google WebView API for Ads references: [iOS](https://developers.google.com/admob/ios/browser/webview/api-for-ads) | [Android](https://developers.google.com/admob/android/browser/webview/api-for-ads) | Full Despia docs: [https://setup.despia.com](https://setup.despia.com)
875
687
 
876
- // Read clipboard data
877
- const clipboardData = await despia('getclipboard://', ['clipboarddata']);
688
+ ---
878
689
 
879
- // Access the clipboard content
880
- const content = clipboardData.clipboarddata;
690
+ ### Web Payment Request API
881
691
 
882
- // Display or process the clipboard content in your application
883
- console.log('Clipboard content:', content);
884
- ```
692
+ Despia polyfills support for the Web Payment Request API, enabling native Apple Pay on iOS and Google Pay on Android directly from your web UI. This is a known pain point with standard WebViews — Despia handles it by importing WebKit's Payment Request support on Android (`androidx.webkit:webkit:1.14.0` with `WebSettingsCompat.setPaymentRequestEnabled`) and the required Google Pay intent queries, and on iOS via WKWebView's built-in Apple Pay support.
885
693
 
886
- **How it works:**
694
+ ```js
695
+ // Standard Web Payment Request API — works in Despia without any native code changes
696
+ const request = new PaymentRequest(
697
+ [{ supportedMethods: 'https://apple.com/apple-pay' }], // or Google Pay method
698
+ {
699
+ total: { label: 'Total', amount: { currency: 'USD', value: '9.99' } }
700
+ }
701
+ );
887
702
 
888
- - **Read clipboard**: Use `getclipboard://` with `['clipboarddata']` to retrieve the current clipboard content
889
- - **Access content**: The clipboard content is available through the `.clipboarddata` property of the returned object
890
- - **Native support**: Works on both iOS and Android platforms
703
+ const result = await request.show();
704
+ await result.complete('success');
705
+ ```
891
706
 
892
- **Perfect for:**
707
+ Apple Pay on iOS and Google Pay on Android both work via the standard Web Payment Request API. No Despia-specific calls required. Reference: [Google Pay in Android WebView](https://developers.google.com/pay/api/android/guides/recipes/using-android-webview)
893
708
 
894
- - Reading text from the clipboard
895
- - Processing clipboard content in your app
896
- - Implementing paste functionality
897
- - Clipboard content validation
709
+ ---
898
710
 
899
- ### Open App Settings Workflow
711
+ ### Web Storage APIs
900
712
 
901
- Open your app's native settings page where users can manage permissions:
713
+ Despia runs your app on a secure origin (`http://localhost` via Local Server, or your remote URL). This means all standard web storage APIs work without any restrictions or workarounds, unlike other hybrid frameworks that require hacks or fall back to `file://` origins:
902
714
 
903
- ```javascript
904
- // First, install the package:
905
- // npm install despia-native
715
+ ```js
716
+ // localStorage works normally
717
+ localStorage.setItem('userId', 'user123');
718
+ const userId = localStorage.getItem('userId');
906
719
 
907
- // Then import it:
908
- import despia from 'despia-native';
720
+ // IndexedDB works normally
721
+ const db = await indexedDB.open('myapp', 1);
909
722
 
910
- // Open native app settings
911
- despia("settingsapp://");
723
+ // Web Crypto works normally
724
+ const key = await crypto.subtle.generateKey(
725
+ { name: 'AES-GCM', length: 256 },
726
+ true,
727
+ ['encrypt', 'decrypt']
728
+ );
912
729
  ```
913
730
 
914
- **How it works:**
731
+ For data that needs to survive uninstall and reinstall, or be locked behind Face ID, use [Identity Vault](#identity-vault) instead.
915
732
 
916
- - **Opens native settings**: Navigates to your app's settings page in the device's system settings
917
- - **Permission management**: Users can manage permissions like notifications, location, camera, microphone, and more
918
- - **One-time prompts**: Great for directing users to activate features that you can only ask once, like location or push notifications
733
+ ---
919
734
 
920
- **Perfect for:**
735
+ ### Local Server
921
736
 
922
- - Directing users to enable location services after they've denied the initial prompt
923
- - Helping users enable push notifications if they initially declined
924
- - Managing camera, microphone, or other permission settings
925
- - Providing a way to access app settings when permission prompts can't be shown again
737
+ > **Built-in OTA updates. True offline support. Host with your existing web hosting. No proprietary infrastructure required.**
926
738
 
927
- **Example Usage:**
739
+ No service workers. No workarounds. The Local Server downloads your complete web build to the device and serves it from a native on-device HTTP server at `http://localhost`. The app loads from device storage at native speed and works completely offline from the first launch after hydration.
928
740
 
929
- ```javascript
930
- // Check if location permission is needed
931
- if (!navigator.geolocation) {
932
- // Open settings so user can enable location
933
- despia("settingsapp://");
934
- }
935
-
936
- // After user denies push notification permission
937
- // Provide a button to open settings
938
- function openNotificationSettings() {
939
- despia("settingsapp://");
940
- }
941
- ```
741
+ When the user has a connection, Despia checks for updates in the background by comparing the `deployed_at` timestamp in your manifest. If a new build is available it downloads silently while the user continues using the app. The update applies on the next launch. If the connection is not stable, the locally cached version is served without interruption.
942
742
 
943
- ### OneSignal Push Notifications Workflow
743
+ You host the manifest and assets on your own infrastructure — Netlify, Vercel, AWS, your own server, anywhere. No proprietary hosting, no per-MAU fees, no vendor lock-in on your delivery pipeline.
944
744
 
945
- Set up OneSignal push notifications with external user IDs to connect your database user IDs with device registrations:
946
-
947
- ```javascript
948
- // First, install the package:
949
- // npm install despia-native
745
+ ```bash
746
+ npm install --save-dev @despia/local
747
+ ```
950
748
 
951
- // Then import it:
952
- import despia from 'despia-native';
749
+ ```js
750
+ // vite.config.js (also available for Webpack, Rollup, Nuxt, SvelteKit, Astro, Remix, esbuild)
751
+ import { defineConfig } from 'vite';
752
+ import { despiaLocalPlugin } from '@despia/local/vite';
953
753
 
954
- // On every app load, set the external user ID
955
- // This connects your logged-in user ID with the device's OneSignal registration
956
- despia(`setonesignalplayerid://?user_id=${YOUR_LOGGED_IN_USER_ID}`);
754
+ export default defineConfig({
755
+ plugins: [
756
+ despiaLocalPlugin({
757
+ outDir: 'dist',
758
+ entryHtml: 'index.html'
759
+ })
760
+ ]
761
+ });
957
762
  ```
958
763
 
959
- **Setup Requirements:**
960
-
961
- 1. **Create a OneSignal account** and configure your app
962
- 2. **Set up iOS (Apple Push Key) and Android (Firebase)** configurations in OneSignal
963
- 3. **Important**: When configuring OneSignal, select **"Native iOS"** and **"Native Android"** platforms since Despia apps are native mobile applications
964
- 4. **Add your OneSignal App ID** to your Despia project settings
965
-
966
- **How it works:**
967
-
968
- - **External User IDs**: External IDs (your database user IDs) are now the default and recommended approach
969
- - **Player IDs**: Player IDs still work but are no longer suggested
970
- - **Device Registration**: Devices are automatically registered in OneSignal when the app is installed
971
- - **User Connection**: Calling `setonesignalplayerid://` on every app load connects your user ID with the device registration
972
-
973
- **Backend Integration Required:**
974
-
975
- You'll need to create a backend endpoint to send notifications using OneSignal's REST API with external user IDs:
976
-
977
- ```javascript
978
- // Backend API endpoint - send to specific user
979
- const sendNotification = async (externalUserId, title, message) => {
980
- const response = await fetch('https://onesignal.com/api/v1/notifications', {
981
- method: 'POST',
982
- headers: {
983
- 'Content-Type': 'application/json',
984
- 'Authorization': 'Basic YOUR_REST_API_KEY'
985
- },
986
- body: JSON.stringify({
987
- app_id: 'ONESIGNAL-APP-ID',
988
- include_external_user_ids: [externalUserId], // Array with single ID
989
- headings: { en: title },
990
- contents: { en: message }
991
- })
992
- });
993
- return response.json();
994
- };
764
+ Or run via CLI after any build:
765
+
766
+ ```bash
767
+ npx despia-local dist
995
768
  ```
996
769
 
997
- **Example Usage:**
770
+ The plugin generates `despia/local.json` in your output directory alongside your assets. Deploy both to your existing web host. That is the entire setup.
998
771
 
999
- ```javascript
1000
- // In your app initialization or login flow
1001
- function initializeApp(userId) {
1002
- // Set external user ID on every app load
1003
- despia(`setonesignalplayerid://?user_id=${userId}`);
1004
-
1005
- // Register for push notifications
1006
- despia('registerpush://');
772
+ ```json
773
+ {
774
+ "entry": "/index.html",
775
+ "deployed_at": "1737225600000",
776
+ "assets": [
777
+ "/index.html",
778
+ "/assets/app.abc123.css",
779
+ "/assets/app.def456.js"
780
+ ]
1007
781
  }
1008
-
1009
- // Send local push notification
1010
- despia('sendlocalpushmsg://push.send?s=60&msg=Hello&!#New Message&!#https://myapp.com');
1011
782
  ```
1012
783
 
1013
- **Important Notes:**
784
+ Full docs: [https://setup.despia.com/local-server/introduction](https://setup.despia.com/local-server/introduction)
1014
785
 
1015
- - This feature requires native capabilities which are fully provided by the `despia-native` npm package
1016
- - No additional native libraries are needed
1017
- - Please follow the installation instructions for the `despia-native` npm package closely
1018
- - Implementation as mentioned is critical for proper push notification delivery
1019
- - Call `setonesignalplayerid://` on every app load to ensure the user ID is always connected
786
+ ---
1020
787
 
1021
- **Perfect for:**
788
+ ### Local CDN
1022
789
 
1023
- - Sending targeted notifications to specific users
1024
- - User-based notification management
1025
- - Cross-device notification delivery
1026
- - Personalized push notification campaigns
790
+ 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.
1027
791
 
1028
- ### Haptic Feedback
792
+ ```js
793
+ // Called by the native runtime when a download completes.
794
+ // This is a global function, not an event listener.
795
+ window.contentServerChange = (item) => {
796
+ // item.local_cdn - localhost URL to use for playback
797
+ // item.cdn - original remote URL
798
+ // item.index - the uniqueId you passed to the write call
799
+ // item.size - file size in bytes
800
+ // item.status - "cached" when complete
801
+ // item.local_path - absolute path on the device
1029
802
 
1030
- All haptic feedback commands have no response - they provide immediate tactile feedback:
803
+ console.log('Cached:', item.index, item.local_cdn);
804
+ addToDownloadsList(item);
805
+ };
1031
806
 
1032
- ```javascript
1033
- // Basic haptic feedback
1034
- despia('lighthaptic://'); // Light haptic feedback - subtle vibration
1035
- despia('heavyhaptic://'); // Heavy haptic feedback - strong vibration
807
+ // Fire and forget. Do not await with a watch key; large downloads take longer than the SDK's
808
+ // 30-second variable-watch window. Use window.contentServerChange to receive the result instead.
809
+ despia(
810
+ `localcdn://write?url=${remoteUrl}&filename=videos/clip.mp4&index=clip_1&push=true&pushmessage="Download complete"`
811
+ );
1036
812
 
1037
- // Contextual haptic feedback
1038
- despia('successhaptic://'); // Success haptic feedback - positive confirmation
1039
- despia('warninghaptic://'); // Warning haptic feedback - attention alert
1040
- despia('errorhaptic://'); // Error haptic feedback - negative feedback
813
+ // Read cached items by ID
814
+ const { cdnItems } = await despia(
815
+ `localcdn://read?index=${encodeURIComponent(JSON.stringify(['clip_1']))}`,
816
+ ['cdnItems']
817
+ );
1041
818
 
1042
- // Use cases:
1043
- // - Button press feedback (light/heavy)
1044
- // - Success notifications (successhaptic)
1045
- // - Warning alerts (warninghaptic)
1046
- // - Error feedback (errorhaptic)
1047
- // - UI interaction confirmation
819
+ // Or query everything in the cache
820
+ const { cdnItems: all } = await despia('localcdn://query', ['cdnItems']);
1048
821
  ```
1049
822
 
1050
- ### Biometric Authentication
1051
-
1052
- Biometric authentication requires setting up callback functions before running the command:
1053
-
1054
- ```javascript
1055
- // Step 1: Set up the biometric authentication SDK
1056
- if (!document.getElementById("bioauth-sdk")) {
1057
- const script = document.createElement("script")
1058
- script.id = "bioauth-sdk"
1059
- script.type = "text/javascript"
1060
- script.textContent = `
1061
- function onBioAuthSuccess() {
1062
- window.bioauthSuccess()
1063
- }
1064
- function onBioAuthFailure(errorCode, errorMessage) {
1065
- window.bioauthFailure(errorCode, errorMessage)
1066
- }
1067
- function onBioAuthUnavailable() {
1068
- window.bioauthUnavailable()
1069
- }
1070
- `
1071
- document.head.appendChild(script)
1072
- }
1073
-
1074
- // Step 2: Define your callback functions
1075
- window.bioauthSuccess = function() {
1076
- if (navigator.userAgent.includes("despia")) {
1077
- console.log("Biometric authentication successful");
1078
- // Handle successful authentication
1079
- // Redirect user, unlock features, etc.
1080
- }
1081
- }
1082
-
1083
- window.bioauthFailure = function(errorCode, errorMessage) {
1084
- if (navigator.userAgent.includes("despia")) {
1085
- console.log("Biometric authentication failed:", errorCode, errorMessage);
1086
- // Handle authentication failure
1087
- // Show error message, fallback to password, etc.
1088
- }
1089
- }
823
+ Use the `local_cdn` URL for playback:
1090
824
 
1091
- window.bioauthUnavailable = function() {
1092
- if (navigator.userAgent.includes("despia")) {
1093
- console.log("Biometric authentication unavailable");
1094
- // Handle when biometric auth is not available
1095
- // Fallback to alternative authentication method
1096
- }
1097
- }
1098
-
1099
- // Step 3: Trigger biometric authentication
1100
- despia('bioauth://');
825
+ ```html
826
+ <video src="http://localhost:7777/localcdn/videos/clip.mp4" controls></video>
1101
827
  ```
1102
828
 
1103
- ### App Information & Device Data
1104
-
1105
- ```javascript
1106
- // Get app version information
1107
- const appInfo = await despia('getappversion://', ['versionNumber', 'bundleNumber']);
1108
- console.log('App Version:', appInfo.versionNumber);
1109
- console.log('Bundle Number:', appInfo.bundleNumber);
829
+ **HTTP upload API** (Local Server only)
1110
830
 
1111
- // Get device UUID (native device ID)
1112
- const deviceData = await despia('get-uuid://', ['uuid']);
1113
- console.log('Device UUID:', deviceData.uuid);
831
+ ```js
832
+ const host = window.location.host; // Do not hardcode the port; it rotates per session
833
+ const fd = new FormData();
834
+ fd.append('file', fileInput.files[0]);
1114
835
 
1115
- // Get store location
1116
- const storeData = await despia('getstorelocation://', ['storeLocation']);
1117
- console.log('Store Location:', storeData.storeLocation);
1118
-
1119
- // Check tracking permission
1120
- const trackingData = await despia('user-disable-tracking://', ['trackingDisabled']);
1121
- console.log('Tracking Disabled:', trackingData.trackingDisabled);
836
+ const res = await fetch(`http://${host}/api/upload`, { method: 'POST', body: fd });
837
+ const data = await res.json();
838
+ // { success: true, fileName: "video.mp4", url: "http://localhost:7777/files/video.mp4" }
1122
839
  ```
1123
840
 
1124
- ### UI Controls & Styling
1125
841
 
1126
- ```javascript
1127
- // Loading spinner controls
1128
- await despia('spinneron://'); // Show loading spinner
1129
- await despia('spinneroff://'); // Hide loading spinner
842
+ | Method | Storage Path | URL Pattern |
843
+ | ------------------ | ------------ | -------------------------------------- |
844
+ | `localcdn://write` | `/localcdn/` | `localhost:{PORT}/localcdn/{filepath}` |
845
+ | `/api/upload` | `/files/` | `localhost:{PORT}/files/{filename}` |
1130
846
 
1131
- // Full screen mode
1132
- await despia('hidebars://on'); // Hide status bar (full screen)
1133
- await despia('hidebars://off'); // Show status bar
1134
847
 
1135
- // Status bar styling
1136
- await despia('statusbarcolor://{255, 255, 255}'); // Set status bar background color (RGB)
1137
- await despia('statusbartextcolor://{black}'); // Set status bar text color (black/white)
1138
- ```
848
+ Full docs: [https://setup.despia.com/local-cdn/introduction](https://setup.despia.com/local-cdn/introduction)
1139
849
 
1140
- ### File & Media Operations
850
+ ---
1141
851
 
1142
- ```javascript
1143
- // Take screenshot (saves to device)
1144
- await despia('takescreenshot://');
852
+ ## Capability Reference
1145
853
 
1146
- // Save image from URL
1147
- await despia('savethisimage://?url=https://example.com/image.jpg');
854
+ A full index of native capabilities built into Despia.
1148
855
 
1149
- // Download file from URL
1150
- await despia('file://https://example.com/document.pdf');
856
+ **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.
1151
857
 
1152
- // Share app with message and URL
1153
- await despia('shareapp://message?=Check%20out%20this%20app&url=https://myapp.com');
1154
- ```
858
+ **Integrated SDK bridges:** RevenueCat (purchases, subscriptions, restore, paywalls), OneSignal (remote push with external user ID mapping), AppsFlyer (attribution, deep linking, event tracking), AdMob (advertising), HealthKit (all major health identifiers).
1155
859
 
1156
- ### Scanning Mode
860
+ **Infrastructure:** ATT compliance, vendor ID tracking, device ID tracking via iCloud KV and Android App Backup, store location access, jailbreak detection with configurable blocking, App Clips, Share Extensions, and Home Widget management from the Despia editor.
1157
861
 
1158
- ```javascript
1159
- // Control scanning mode
1160
- await despia('scanningmode://auto'); // Auto scanning mode
1161
- await despia('scanningmode://on'); // Enable scanning
1162
- await despia('scanningmode://off'); // Disable scanning
1163
- ```
1164
-
1165
- ### App Reset
862
+ **Native web interception:** `<input type="file">` routes to a native action sheet. The `capture` attribute opens the native camera. `accept="image/*"` or `accept="video/*"` opens the native media gallery. Deeplinks and HTTPS deeplinks are handled natively.
1166
863
 
1167
- ```javascript
1168
- // Reset app (use with caution)
1169
- await despia('reset://');
1170
- ```
864
+ ---
1171
865
 
1172
- ### Native Safe Area
866
+ ## Safe Area
1173
867
 
1174
- Access native safe area insets via CSS custom properties:
868
+ Despia exposes top and bottom safe area insets as CSS custom properties set by the native runtime.
1175
869
 
1176
870
  ```css
1177
- /* Use native safe area insets in your CSS */
1178
- .my-element {
871
+ .header {
1179
872
  padding-top: var(--safe-area-top);
873
+ }
874
+
875
+ .footer {
1180
876
  padding-bottom: var(--safe-area-bottom);
1181
877
  }
1182
878
 
1183
- /* Full height with safe area consideration */
1184
879
  .full-height {
1185
880
  height: calc(100vh - var(--safe-area-top) - var(--safe-area-bottom));
1186
881
  }
1187
882
  ```
1188
883
 
1189
- **Note:** Despia only supports top and bottom safe area insets. Left and right safe area variables are not available.
884
+ Note: left and right safe area variables are not available.
1190
885
 
1191
- These CSS variables are automatically provided by the Despia native runtime and represent the device's safe area insets (notches, home indicators, etc.).
886
+ ---
1192
887
 
888
+ ## Extending the Runtime
1193
889
 
890
+ A common concern when adopting any hybrid framework is lock-in: can you escape the custom protocol API later?
1194
891
 
1195
- ## API Reference
892
+ 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.
1196
893
 
1197
- ### `despia(command, watch?)`
894
+ **On the bridge architecture**
1198
895
 
1199
- - **command** (string): The Despia protocol command (e.g., `'lighthaptic://'`)
1200
- - **watch** (string[], optional): Array of variable names to watch for in the response
896
+ 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.
1201
897
 
1202
- Returns a Promise that resolves when all watched variables are available:
1203
- - **Single variable**: 30-second timeout with observation. `null` values resolve immediately (useful for error/not-found signals). Other empty placeholders (`undefined`, `"n/a"`, `{}`, `[]`) are ignored. Promise always resolves; on timeout it resolves with `undefined`.
1204
- - **Multiple variables**: Uses VariableTracker with 5-minute auto-cleanup. All variables must have non-null values. Any `null` value blocks resolution until all variables have real values.
898
+ 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.
1205
899
 
1206
- ### Timeout behavior
900
+ 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.
1207
901
 
1208
- When you call `despia(command, ['someVariable'])`, the SDK waits up to 30 seconds for
1209
- `window.someVariable` to be set by the native runtime. If it appears earlier, the
1210
- Promise resolves with that value. If it is never set, the Promise still resolves
1211
- after 30 seconds with `undefined` and a timeout is logged to the console. This
1212
- prevents hanging Promises for long-running or failing native operations.
902
+ **iOS: WebViewController.swift**
1213
903
 
1214
- ### Fresh-data behavior
904
+ ```swift
905
+ if requestURL.absoluteString.hasPrefix("mycustom://") {
906
+ let result = runMyNativeCode()
907
+ webView.evaluateJavaScript("window.myResult = '\(result)';")
908
+ decisionHandler(.cancel)
909
+ return
910
+ }
911
+ ```
1215
912
 
1216
- Before observing, watched variables are cleared to avoid resolving on stale values.
1217
- The observer behavior differs by variable count:
913
+ **Android: MainActivity.java**
1218
914
 
1219
- - **Single variable**: `null` is treated as a valid resolution value (useful for error/not-found signals). The observer ignores other empty placeholders (`undefined`, `"n/a"`, `{}`, `[]`) and requires the value to change from its baseline before resolving.
915
+ ```java
916
+ if (url.startsWith("mycustom://")) {
917
+ String result = runMyNativeCode();
918
+ webView.evaluateJavascript("window.myResult = '" + result + "';", null);
919
+ return true;
920
+ }
921
+ ```
1220
922
 
1221
- - **Multiple variables**: All variables must have non-null values. The observer ignores empty placeholders (`undefined`, `null`, `"n/a"`, `{}`, `[]`) and requires all values to change from their baseline before resolving.
923
+ **Call it from your web app**
1222
924
 
1223
- This ensures each call waits for a fresh write from the native side.
925
+ ```js
926
+ import despia from 'despia-native';
1224
927
 
1225
- ### Direct Property Access
928
+ const data = await despia('mycustom://', ['myResult']);
929
+ console.log(data.myResult);
930
+ ```
1226
931
 
1227
- Access any window variable directly through the despia object:
932
+ Adding custom native code requires exporting the project and deploying outside of Despia's hosted CI/CD pipeline. Despia offers full Xcode and Android Studio project export for developers who need it. You own the code and can build and ship entirely independently at any point.
1228
933
 
1229
- ```javascript
1230
- despia.variableName // Equivalent to window.variableName
1231
- ```
934
+ Each lifetime Despia license applies to one specific Despia project bound to one bundle ID, and grants export, editing, self-hosting, and custom CI/CD rights for that project only. Associated repositories must remain private. Reuse beyond that scope requires separate licensing or white-label terms.
1232
935
 
1233
- ## Despia Protocol Format
936
+ Projects and their licensing rights can be transferred between Despia accounts. Contact [license@despia.com](mailto:license@despia.com) for transfers or licensing questions, and [whitelabel@despia.com](mailto:whitelabel@despia.com) for white-label use cases.
1234
937
 
1235
- Despia uses a simple protocol format for all native integrations:
938
+ > **Coming mid/late 2026:** An official open source Despia Extension system is currently in active development. It will allow developers to build and distribute custom native capabilities that work directly within the Despia editor, with no Xcode or Android Studio required.
1236
939
 
1237
- ```
1238
- feature://action?parameters
1239
- ```
940
+ ---
1240
941
 
1241
- Examples:
1242
- - `lighthaptic://`
1243
- - `getappversion://`
1244
- - `revenuecat://purchase?external_id=user_777&product=monthly_premium`
1245
- - `revenuecat://launchPaywall?external_id=user_777&offering=default`
1246
- - `getpurchasehistory://`
1247
- - `getclipboard://`
1248
- - `settingsapp://`
1249
- - `setonesignalplayerid://?user_id=user123`
1250
- - `registerpush://`
1251
- - `setvault://?key=userId&value=user123&locked=false`
1252
- - `readvault://?key=userId`
1253
- - `writevalue://{JSON-ENCODED-STRING}`
1254
- - `readvalue://`
1255
- - `oauth://?url=https://provider.com/oauth/authorize`
1256
- - `requestcontactpermission://`
1257
- - `savethisimage://?url=https://example.com/image.jpg`
1258
-
1259
- ## Available Despia Features
1260
-
1261
- Your app can access these native features:
1262
-
1263
- - **Native Widgets** - Create widgets with SVG and refresh time
1264
- - **In-App Purchases** - RevenueCat integration with external user IDs
1265
- - **Restore Purchases** - Retrieve purchase history from App Store and Google Play
1266
- - **Contact Access** - Request permissions and read contacts
1267
- - **Background Location** - Native tracking with browser geolocation API
1268
- - **Push Notifications** - OneSignal integration with external user IDs and local push messages
1269
- - **Haptic Feedback** - Light, heavy, success, warning, and error feedback
1270
- - **App Information** - Version numbers, bundle numbers, device UUID
1271
- - **Clipboard Access** - Read clipboard content from device
1272
- - **Screenshots** - Take device screenshots
1273
- - **Scanning Mode** - Auto, on, and off scanning controls
1274
- - **Store Location** - Get store location data
1275
- - **File Operations** - Save images and download files
1276
- - **Identity Vault** - Persistent cross-device storage with optional biometric protection
1277
- - **Local Storage** - Cross-platform device storage (cleared on uninstall)
1278
- - **OAuth Authentication** - Secure OAuth flows with native browser sessions
1279
- - **App Control** - Reset app and disable tracking
1280
- - **App Settings** - Open native app settings for permission management
1281
- - **UI Controls** - Loading spinners and full screen mode
1282
- - **Sharing** - Share app with custom messages and URLs
1283
- - **Status Bar** - Control colors and text colors
1284
- - **Biometric Authentication** - Native biometric auth with callbacks
1285
-
1286
- ## TypeScript Support
1287
-
1288
- Full TypeScript definitions are included:
1289
-
1290
- ```typescript
1291
- import despia from 'despia-native';
942
+ ## Web Apps vs React Native
1292
943
 
1293
- // Type-safe usage with Despia commands
1294
- const result: { versionNumber: string; bundleNumber: string } = await despia(
1295
- 'getappversion://',
1296
- ['versionNumber', 'bundleNumber']
1297
- );
944
+ This SDK is for web apps running inside the Despia runtime: React, Vue, Angular, Svelte, Next.js, Vite, Nuxt, and vanilla JavaScript.
1298
945
 
1299
- // Direct property access
1300
- const deviceInfo: any = despia.deviceInfo;
1301
- ```
946
+ It is not for React Native, Expo, or native mobile development.
1302
947
 
1303
- ## Integration with Despia
948
+ ---
1304
949
 
1305
- Despia operates through a streamlined protocol handler system, allowing you to invoke native features using the global `window.despia` object. This npm package is the JavaScript SDK that makes your web app communicate with Despia's native runtime. The SDK provides:
950
+ ## Publishing to the App Store and Google Play
1306
951
 
1307
- - **Command Queuing** - Sequential execution of Despia commands via `window.despia` setter
1308
- - **Variable Watching** - Async monitoring of response variables
1309
- - **Hybrid Framework Compatible** - Works with Despia's hybrid app framework
1310
- - **Direct Access** - Proxy-based access to window variables
952
+ Despia provides fully automated iOS and Android store deployment from the web editor. No Mac required. One-click deployment spins up Mac Mini build infrastructure in the cloud, handles code signing, provisioning, and submits to both the App Store and Google Play — entirely from a browser.
1311
953
 
1312
- ### How It Works
1313
- Despia's protocol handler system eliminates the need for complex libraries or dependencies, making it compatible across various frameworks and platforms. The SDK uses the setter pattern to execute commands:
954
+ Full deployment docs: [https://setup.despia.com/deployment/apple-ios/automatic](https://setup.despia.com/deployment/apple-ios/automatic)
1314
955
 
1315
- ```javascript
1316
- // When you call:
1317
- despia('lighthaptic://');
956
+ ---
957
+
958
+ ## Open Source
959
+
960
+
961
+ | Package | Description | License |
962
+ | -------------------------------------------------------------------------- | --------------------- | ------- |
963
+ | [despia-native](https://www.npmjs.com/package/despia-native) | JavaScript SDK | MIT |
964
+ | [@despia/local](https://www.npmjs.com/package/@despia/local) | Offline asset bundler | MIT |
965
+ | [despia-version-guard](https://www.npmjs.com/package/despia-version-guard) | OTA version gating | MIT |
1318
966
 
1319
- // It internally executes:
1320
- window.despia = 'lighthaptic://';
1321
- ```
1322
967
 
1323
- This streamlined approach triggers Despia's native runtime to handle the native command, providing seamless access to device capabilities directly from your web codebase.
968
+ Native capability implementations are written in Swift and Java and included in full on project export.
969
+
970
+ ---
971
+
972
+ ## Support
973
+
974
+ For questions or concerns regarding this package, contact the Despia team at [npm@despia.com](mailto:npm@despia.com).
975
+
976
+ ---
1324
977
 
1325
978
  ## License
1326
979