---
title: Native Sheets in NativeScript
sidebar_label: NativeScript
pagination_prev: demos/static/index
pagination_next: demos/desktop/index
sidebar_position: 2
sidebar_custom_props:
summary: JS + Native Elements
---
import current from '/version.js';
import CodeBlock from '@theme/CodeBlock';
export const g = {style: {color:"green"}};
export const r = {style: {color:"red"}};
export const y = {style: {color:"gold"}};
[NativeScript](https://nativescript.org/) is a mobile app framework. It builds
iOS and Android apps that use JavaScript for describing layouts and events.
[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
data from spreadsheets.
This demo uses NativeScript and SheetJS to process and generate spreadsheets.
We'll explore how to load SheetJS in a NativeScript app; parse and generate
spreadsheets stored on the device; and fetch and parse remote files.
The ["Complete Example"](#complete-example) creates an app that looks like the
screenshots below:
:::info pass
The discussion covers the NativeScript + Angular integration. Familiarity with
Angular and TypeScript is assumed.
:::
:::note Tested Deployments
This demo was tested in the following environments:
**Real Devices**
| OS | Device | NS | Date |
|:-----------|:--------------------|:---------|:-----------|
| Android 30 | NVIDIA Shield | `8.9.2` | 2025-05-06 |
| iOS 15.1 | iPad Pro | `8.9.2` | 2025-05-06 |
**Simulators**
| OS | Device | NS | Dev Platform | Date |
|:-----------|:--------------------|:---------|:-------------|:-----------|
| Android 35 | Pixel 9 Pro XL | `8.9.2` | `darwin-x64` | 2025-05-06 |
| iOS 18.4 | iPhone 16 Pro Max | `8.9.2` | `darwin-x64` | 2025-05-06 |
| Android 35 | Pixel 9 | `8.8.3` | `win11-x64` | 2024-12-21 |
| Android 35 | Pixel 9 | `8.8.3` | `linux-x64` | 2025-01-02 |
:::
:::danger Telemetry
Before starting this demo, manually disable telemetry.
NativeScript 8.6.1 split the telemetry into two parts: "usage" and "error". Both
must be disabled separately:
```bash
npx -y -p nativescript ns usage-reporting disable
npx -y -p nativescript ns error-reporting disable
```
To verify telemetry was disabled:
```bash
npx -y -p nativescript ns usage-reporting status
npx -y -p nativescript ns error-reporting status
```
:::
## Integration Details
The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be
imported from any component or script in the app.
The `@nativescript/core/file-system` package provides classes for file access.
The `File` class does not support binary data, but the file access singleton
from `@nativescript/core` does support reading and writing `ArrayBuffer` data.
Reading and writing data require a URL. The following snippet searches typical
document folders for a specified filename:
```ts
import { Folder, knownFolders, path } from '@nativescript/core/file-system';
function get_url_for_filename(filename: string): string {
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
return path.normalize(target.path + "///" + filename);
}
```
### App Configuration
Due to privacy concerns, apps must request file access. There are special APIs
for accessing data and are subject to change in future platform versions.
Technical Details (click to show)
**iOS**
The following key/value pairs must be added to `Info.plist`:
```xml title="App_Resources/iOS/Info.plist (add highlighted lines)"
UIFileSharingEnabledLSSupportsOpeningDocumentsInPlace
```
---
**Android**
Android security has evolved over the years. In newer Android versions, the
following workarounds were required:
- `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` allow apps to access
files outside of the app scope. These are required for scoped storage access.
When the demo was last tested, this option was enabled by default.
- `android:requestLegacyExternalStorage="true"` enabled legacy behavior in some
older releases.
The manifest is saved to `App_Resources/Android/src/main/AndroidManifest.xml`:
```xml title="App_Resources/Android/src/main/AndroidManifest.xml (add highlighted lines)"
android:requestLegacyExternalStorage="true"
android:name="com.tns.NativeScriptApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:hardwareAccelerated="true">
```
- Permissions must be explicitly requested.
`@nativescript-community/perms` is a community module for managing permissions:
```ts title="App script or component"
import { request } from '@nativescript-community/perms';
import { File } from '@nativescript/core/file-system';
```
Storage access must be requested before writing data:
```ts title="App script or component (before writing file)"
/* request permissions */
const res = await request('storage');
```
The external paths can be resolved using the low-level APIs:
```ts title="App script or component (writing to downloads folder)"
/* find Downloads folder */
const dl_dir = android.os.Environment.DIRECTORY_DOWNLOADS;
const dl = android.os.Environment.getExternalStoragePublicDirectory(dl_dir).getAbsolutePath();
/* write to file */
File.fromPath(dl + "/SheetJSNS.xls").writeSync(data);
```
### Reading Local Files
`getFileAccess().readBufferAsync` can read data into an `ArrayBuffer` object.
The SheetJS `read` method[^1] can parse this data into a workbook object.[^2]
```ts
import { getFileAccess } from '@nativescript/core';
import { read } from 'xlsx';
/* find appropriate path */
const url = get_url_for_filename("SheetJSNS.xls");
/* get data */
const ab: ArrayBuffer = await getFileAccess().readBufferAsync(url);
/* read workbook */
const wb = read(ab);
```
After parsing into a workbook, the `sheet_to_json`[^3] method can generate row
data objects:
```ts
import { utils } from 'xlsx';
/* grab first sheet */
const wsname: string = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
/* generate array of row objects */
const data = utils.sheet_to_json(ws);
```
### Writing Local Files
The SheetJS `write` method[^4] with the option `type: "binary"` will generate
`Uint8Array` objects. `getFileAccess().writeBufferAsync` can write data from a
`Uint8Array` object to the device.
iOS supports `Uint8Array` directly but Android requires a true array of numbers:
```ts
import { getFileAccess } from '@nativescript/core';
import { write } from 'xlsx';
/* find appropriate path */
const url = get_url_for_filename("SheetJSNS.xls");
/* generate Uint8Array */
const u8: Uint8Array = write(wb, { bookType: 'xls', type: 'binary' });
/* attempt to save Uint8Array to file */
await getFileAccess().writeBufferAsync(url, global.isAndroid ? (Array.from(u8) as any) : u8);
```
A worksheet can be generated from an array of row objects with the SheetJS
`json_to_sheet` method[^5]. After generating an array, the `book_new` and
`book_append_sheet` methods[^6] can create the workbook.
### Fetching Remote Files
`getFile` from `@nativescript/core/http` can download files. After storing the
file in a temporary folder, `getFileAccess().readBufferAsync` can read the data
and the SheetJS `read` method[^7] can parse the file:
```ts
import { knownFolders, path, getFileAccess } from '@nativescript/core'
import { getFile } from '@nativescript/core/http';
import { read } from 'xlsx';
/* generate temporary path for the new file */
const temp: string = path.join(knownFolders.temp().path, "pres.xlsx");
/* download file */
const file = await getFile("https://docs.sheetjs.com/pres.xlsx", temp)
/* get data */
const ab: ArrayBuffer = await getFileAccess().readBufferAsync(file.path);
/* read workbook */
const wb = read(ab);
```
## Complete Example
### Platform Configuration
0) Disable telemetry:
```bash
npx -y -p nativescript ns usage-reporting disable
npx -y -p nativescript ns error-reporting disable
```
1) Follow the official Environment Setup instructions[^8].
:::caution pass
In previous test runs, NativeScript did not support the latest Android API.
The error message from `npx -y -p nativescript ns doctor android` clearly stated
supported versions:
✖ No compatible version of the Android SDK Build-tools are installed on your system. You can install any version in the following range: '>=23 <=33'.
If NativeScript does not properly support the latest API level, an older API
version should be installed using Android Studio.
In a previous test run, the following packages were required:
- `Android 13.0 ("Tiramisu")` API Level `33`
- `Android SDK Build-Tools` Version `33.0.2`
It is recommended to install the SDK Platform and corresponding Android SDK
Build-Tools for the latest supported API level.
:::
2) Test the local system configuration for Android development:
```bash
npx -y -p nativescript ns doctor android
```
In the last macOS test, the following output was displayed:
Expected output (click to hide)
✔ Getting environment information
No issues were detected.✔ Your ANDROID_HOME environment variable is set and points to correct directory.
✔ Your adb from the Android SDK is correctly installed.
✔ The Android SDK is installed.
✔ A compatible Android SDK for compilation is found.
✔ Javac is installed and is configured properly.
✔ The Java Development Kit (JDK) is installed and is configured properly.
✔ Getting NativeScript components versions information...
✔ Component nativescript has 8.9.2 version and is up to date.
3) Test the local system configuration for iOS development (macOS only):
```bash
npx -y -p nativescript ns doctor ios
```
In the last macOS test, the following output was displayed:
Expected output (click to hide)
✔ Getting environment information
No issues were detected.✔ Xcode is installed and is configured properly.
✔ xcodeproj is installed and is configured properly.
✔ CocoaPods are installed.
✔ CocoaPods update is not required.
✔ CocoaPods are configured properly.
✔ Your current CocoaPods version is newer than 1.0.0.
✔ Python installed and configured correctly.
✔ Xcode version 16.3.0 satisfies minimum required version 10.
✔ Getting NativeScript components versions information...
✔ Component nativescript has 8.9.2 version and is up to date.
### Base Project
4) Create a skeleton NativeScript + Angular app:
```bash
npx -y -p nativescript ns create SheetJSNS --ng
```
5) Launch the app in the Android simulator to verify the app:
```bash
cd SheetJSNS
npx -y -p nativescript ns run android
```
(this may take a while)
Once the simulator launches and the test app is displayed, end the script by
selecting the terminal and pressing CTRL+C. On Windows, if
prompted to `Terminate batch job`, type Y and press Enter.
:::note pass
If the emulator is not running, `nativescript` may fail with the message:
```
Emulator start failed with: No emulator image available for device identifier 'undefined'.
```
:::
:::caution pass
In the most recent test, the build failed with an exception:
```
WARNING: A restricted method in java.lang.System has been called
WARNING: java.lang.System::load has been called by net.rubygrapefruit.platform.internal.NativeLibraryLoader in an unnamed module (file:/Users/sheetjs/.gradle/wrapper/dists/gradle-8.7-bin/bhs2wmbdwecv87pi65oeuq5iu/gradle-8.7/lib/native-platform-0.22-milestone-25.jar)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
```
**The NativeScript Gradle version is incompatible with Java 24!**
It is strongly recommended to roll back to Java 21.
:::
### Add SheetJS
:::note pass
The goal of this section is to display the SheetJS library version number.
:::
6) From the project folder, install the SheetJS NodeJS module:
{`\
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`}
7) Edit `src/app/item/items.component.ts` so that the component imports the
SheetJS version string and adds it to a `version` variable in the component:
```ts title="src/app/item/items.component.ts (add highlighted lines)"
// highlight-next-line
import { version } from 'xlsx';
import { Component, OnInit } from '@angular/core'
// ...
export class ItemsComponent implements OnInit {
// highlight-next-line
version = `SheetJS - ${version}`;
itemService = inject(ItemService)
page = inject(Page)
// ...
```
8) Edit the template `src/app/item/items.component.html` to reference `version`
in the title of the action bar:
```xml title="src/app/item/items.component.html (edit highlighted line)"
```
9) End the script and relaunch the app in the Android simulator:
```bash
npx -y -p nativescript ns run android
```
The title bar should show the version.

### Local Files
10) Add the Import and Export buttons to the template:
```xml title="src/app/item/items.component.html (add highlighted lines)"
```
11) Add the `import` and `export` methods in the component script:
```ts title="src/app/item/items.component.ts (add highlighted lines)"
// highlight-start
import { version, utils, read, write } from 'xlsx';
import { Dialogs, getFileAccess } from '@nativescript/core';
import { Folder, knownFolders, path } from '@nativescript/core/file-system';
// highlight-end
import { Component, NO_ERRORS_SCHEMA, inject } from '@angular/core'
import { NativeScriptCommonModule, NativeScriptRouterModule } from '@nativescript/angular'
import { Page } from '@nativescript/core'
import { ItemService } from './item.service'
// highlight-start
function get_url_for_filename(filename: string): string {
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
return path.normalize(target.path + "///" + filename);
}
// highlight-end
// ...
export class ItemsComponent {
version = `SheetJS - ${version}`;
itemService = inject(ItemService)
page = inject(Page)
// highlight-start
/* Import button */
async import() {
}
/* Export button */
async export() {
}
// highlight-end
// ...
```
12) End the script and relaunch the app in the Android simulator:
```bash
npx -y -p nativescript ns run android
```
Two buttons should appear just below the header:

13) Implement import and export by adding the highlighted lines:
```ts title="src/app/item/items.component.ts (add highlighted lines)"
/* Import button */
async import() {
// highlight-start
/* find appropriate path */
const url = get_url_for_filename("SheetJSNS.xls");
try {
await Dialogs.alert(`Attempting to read from SheetJSNS.xls at ${url}`);
/* get data */
const ab: ArrayBuffer = await getFileAccess().readBufferAsync(url);
/* read workbook */
const wb = read(ab);
/* grab first sheet */
const wsname: string = wb.SheetNames[0];
const ws = wb.Sheets[wsname];
/* update table */
this.itemService.items.set(utils.sheet_to_json(ws));
} catch(e) { await Dialogs.alert(e.message); }
// highlight-end
}
/* Export button */
async export() {
// highlight-start
/* find appropriate path */
const url = get_url_for_filename("SheetJSNS.xls");
try {
/* create worksheet from data */
const ws = utils.json_to_sheet(this.itemService.items());
/* create workbook from worksheet */
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, "Sheet1");
/* generate Uint8Array */
const u8: Uint8Array = write(wb, { bookType: 'xls', type: 'buffer' });
/* attempt to save Uint8Array to file */
await getFileAccess().writeBufferAsync(url, global.isAndroid ? (Array.from(u8) as any) : u8);
await Dialogs.alert(`Wrote to SheetJSNS.xls at ${url}`);
} catch(e) { await Dialogs.alert(e.message); }
// highlight-end
}
```
### Android
14) Launch the app in the Android Simulator:
```bash
npx -y -p nativescript ns run android
```
If the app does not automatically launch, manually open the `SheetJSNS` app.
15) Tap "Export File". A dialog will print where the file was written. Typically
the URL is `/data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls`
16) Pull the file from the simulator. The following commands should be run in a
new terminal or PowerShell window:
```bash
adb root
adb pull /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls SheetJSNS.xls
```
If the emulator cannot be rooted, the following command works in macOS:
```bash
adb shell "run-as org.nativescript.SheetJSNS cat /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls" > SheetJSNS.xls
```
:::caution pass
In the most recent `win11-x64` test, the generated file was corrupt. This is a
known issue with Windows redirects. The solution is to generate a Base64-encoded
string and decode using PowerShell:
```bash
adb shell "run-as org.nativescript.SheetJSNS base64 /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls" > SheetJSNS.xls.b64
$b64 = Get-Content -Path .\SheetJSNS.xls.b64 -Raw
$bytes = [Convert]::FromBase64String($b64)
[System.IO.File]::WriteAllBytes("SheetJSNS.xls", $bytes)
```
:::
17) Open `SheetJSNS.xls` with a spreadsheet editor.
After the header row, insert a row and make the following assignments:
- Set cell `A2` to `0`
- Set cell `B2` to `SheetJS` (type `'SheetJS` in the formula bar)
- Set cell `C2` to `Library` (type `'Library` in the formula bar)
After making the changes, the worksheet should look like the following:
```text
id | name | role
# highlight-next-line
0 | SheetJS | Library
...
```
18) Push the file back to the simulator:
```bash
adb push SheetJSNS.xls /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls
```
If the emulator cannot be rooted, the following command works in macOS:
```bash
dd if=SheetJSNS.xls | adb shell "run-as org.nativescript.SheetJSNS dd of=/data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls"
```
:::caution pass
In the most recent `win11-x64` test, neither workaround worked. The solution is
to generate a Base64-encoded string and decode in `adb`. After closing Excel and
saving the `SheetJSNS.xls` file, run the following commands:
```bash
$bytes = [IO.File]::ReadAllBytes(".\SheetJSNS.xls")
$b64 = [Convert]::ToBase64String($bytes)
echo $b64 | adb shell "run-as org.nativescript.SheetJSNS base64 -d | run-as org.nativescript.SheetJSNS dd of=/data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls"
```
:::
19) Tap "Import File". A dialog will print the path of the file that was read.
The first item in the list will change.

### iOS
:::danger pass
**iOS testing can only be performed on Apple hardware running macOS!**
Xcode and iOS simulators are not available on Windows or Linux.
Scroll down to ["Fetching Files"](#android-device) for Android device testing.
:::
20) Launch the app in the iOS Simulator:
```bash
npx -y -p nativescript ns run ios
```
21) Tap "Export File". A dialog will print where the file was written.
22) Open the file with a spreadsheet editor.
After the header row, insert a row and make the following assignments:
- Set cell `A2` to `0`
- Set cell `B2` to `SheetJS` (type `'SheetJS` in the formula bar)
- Set cell `C2` to `Library` (type `'Library` in the formula bar)
After making the changes, the worksheet should look like the following:
```text
id | name | role
# highlight-next-line
0 | SheetJS | Library
...
```
23) Restart the app after saving the file.
24) Tap "Import File". A dialog will print the path of the file that was read.
The first item in the list will change:

### Fetching Files
25) Replace `item.service.ts` with the following:
```ts title="src/app/item/item.service.ts"
import { read, utils } from 'xlsx';
import { Injectable, signal, effect } from '@angular/core'
import { knownFolders, path, getFileAccess } from '@nativescript/core';
import { getFile } from '@nativescript/core/http';
import { Item } from './item'
interface IPresident { Name: string; Index: number };
@Injectable({ providedIn: 'root' })
export class ItemService {
items = signal([]);
constructor() { effect(() => { (async() => {
/* fetch https://docs.sheetjs.com/pres.xlsx */
const temp: string = path.join(knownFolders.temp().path, "pres.xlsx");
const ab = await getFile("https://docs.sheetjs.com/pres.xlsx", temp)
/* read the temporary file */
const wb = read(await getFileAccess().readBufferAsync(ab.path));
/* translate the first worksheet to the required Item type */
const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
/* update state */
this.items.set(data.map((pres, id) => ({id, name: pres.Name, role: ""+pres.Index} as Item)));
})(); }); }
getItem(id: number): Item {
return this.items().find((item) => item.id === id)
}
}
```
26) End the script and relaunch the app in the Android simulator:
```bash
npx -y -p nativescript ns run android
```
The app should show Presidential data.
### Android Device
27) Connect an Android device using a USB cable.
If the device asks to allow USB debugging, tap "Allow".
28) Close any Android / iOS emulators.
29) Enable "Legacy External Storage" in the Android app. The manifest is stored
at `App_Resources/Android/src/main/AndroidManifest.xml`:
```xml title="App_Resources/Android/src/main/AndroidManifest.xml (add highlighted line)"
android:requestLegacyExternalStorage="true"
android:name="com.tns.NativeScriptApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme"
android:hardwareAccelerated="true">
```
30) Install the `@nativescript-community/perms` dependency:
```bash
npm i --save @nativescript-community/perms
```
31) Add the highlighted lines to `items.component.ts`:
- Import `File` from NativeScript core and `request` from the new dependency:
```ts title="items.component.ts (add highlighted lines)"
import { Dialogs, getFileAccess, Utils } from '@nativescript/core';
// highlight-start
import { request } from '@nativescript-community/perms';
import { Folder, knownFolders, path, File } from '@nativescript/core/file-system';
// highlight-end
import { Component, OnInit } from '@angular/core'
// ...
```
- Add a new write operation to the `export` method:
```ts title="items.component.ts (add highlighted lines)"
/* attempt to save Uint8Array to file */
await getFileAccess().writeBufferAsync(url, global.isAndroid ? (Array.from(u8) as any) : u8);
await Dialogs.alert(`Wrote to SheetJSNS.xls at ${url}`);
/* highlight-start */
if(global.isAndroid) {
/* request permissions */
const res = await request('storage');
/* write to Downloads folder */
const dl = android.os.Environment.getExternalStoragePublicDirectory(android.os.Environment.DIRECTORY_DOWNLOADS).getAbsolutePath();
File.fromPath(dl + "/SheetJSNS.xls").writeSync(Array.from(u8));
}
/* highlight-end */
} catch(e) { await Dialogs.alert(e.message); }
```
32) Build APK and run on device:
```bash
npx -y -p nativescript ns run android
```
If the Android emulators are closed and an Android device is connected, the last
command will build an APK and install on the device.
Android Device Testing (click to hide)
When the app launches, if the SheetJS library is loaded and if the device is
connected to the Internet, a list of Presidents should be displayed.
Tap "Export File". The app will show an alert. Tap "OK".
Switch to the "Files" app and open the "Downloads" folder. There should be a new
file named `SheetJSNS.xls`.
### iOS Device
33) Connect an iOS device using a USB cable
34) Close any Android / iOS emulators.
35) Enable developer code signing certificates:
Open `platforms/ios/SheetJSNS.xcodeproj/project.xcworkspace` in Xcode. Select
the "Project Navigator" and select `SheetJSNS`. In the main view, select the
`SheetJSNS` target. Click "Signing & Capabilities". Under "Signing", select a
team in the dropdown menu.
:::caution pass
When this demo was last tested, Xcode repeatedly crashed.
The issue was resolved by cleaning the project:
```bash
npx -y -p nativescript ns platform clean ios
```
:::
36) Add the following key/value pairs to `Info.plist`:
```xml title="App_Resources/iOS/Info.plist (add highlighted lines)"
UIFileSharingEnabledLSSupportsOpeningDocumentsInPlace
```
37) Run on device:
```bash
npx -y -p nativescript ns run ios
```
:::info pass
If this is the first time testing an app on a device, the certificate must be
trusted on the device:
Under "Settings" > "General" > "VPN & Device Management", there should be a
"Apple Development" certificate in the "DEVELOPER APP" section. Select the
certificate and confirm that "SheetJSNS" is listed under "APPS". Tap "Trust ..."
and tap "Trust" in the popup.
:::
iOS Device Testing (click to hide)
When the app launches, if the SheetJS library is loaded and if the device is
connected to the Internet, a list of Presidents should be displayed.
Tap "Export File". The app will show an alert. Tap "OK".
Switch to the "Files" app and repeatedly tap "<". In the "Browse" window, tap
"On My iPhone". There should be a new folder named "SheetJSNS". Tap the folder
and look for the file named `SheetJSNS.xls`.
[^1]: See [`read` in "Reading Files"](/docs/api/parse-options)
[^2]: See ["Workbook Object"](/docs/csf/book)
[^3]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^4]: See [`write` in "Writing Files"](/docs/api/write-options)
[^5]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
[^6]: See ["Workbook Helpers" in "Utilities"](/docs/api/utilities/wb) for details on `book_new` and `book_append_sheet`.
[^7]: See [`read` in "Reading Files"](/docs/api/parse-options)
[^8]: See ["Local setup"](https://docs.nativescript.org/setup/#local-setup) in the NativeScript documentation. For Windows and Linux, follow the "Android" instructions. For macOS, follow both the iOS and Android instructions.