# What is Natively?

Natively is a no-code tool that converts your website into a native iOS and Android app.

Instead of rebuilding your product from scratch, Natively wraps your website inside a native app shell using WebView, an embedded browser built into every iOS and Android device. Your web content runs inside it exactly as it does in a mobile browser, while the native shell gives you access to device features your website alone can't reach.

#### **Add native features to your app**

Once your app is running in Natively, you can go beyond what a website offers. A mobile app unlocks capabilities that a browser simply can't provide - sending [Push Notifications](/natively-platform/features/notifications) to bring users back, accepting [In-App Purchases](/natively-platform/features/purchases), scanning [QR codes](/guides/integration/scanner-qr-barcode), reading [NFC tags](/natively-platform/features/nfc), or verifying identity with [Biometrics](/guides/integration/biometrics-and-credentials). These are just a few examples of what becomes possible.

Natively supports a wide range of native features that you can enable as your app grows. Most features can be configured directly from the Natively Dashboard. For features that require deeper integration, Natively provides a JavaScript SDK and a Bubble plugin.

[See the full feature list →](/guides/integration)

#### **Your website updates - your app updates too**

Because your app loads content from your website, any change you publish to your site is immediately reflected in the app - no rebuild, no resubmission to the App Store or Google Play required. This means you can ship content updates, fix bugs, and iterate on your product at web speed, without going through the app store review process each time.

Rebuilds are only needed when you make changes to your Natively Dashboard configuration - like enabling a new feature, updating your app icon, or modifying permissions.

{% embed url="<https://www.youtube.com/watch?t=4s&v=Qen7t32uHCs>" %}
Natively - Convert your website into App without coding
{% endembed %}


# Why Natively?

#### From website to app in minutes

No development team, no months of work, no large budget. With Natively, you can go from your existing website to a published iOS and Android app in minutes - no coding skills required.

#### **Works with any website**

Natively works with any website or web app that runs over HTTPS - whether you built it with Lovable, Base44, Replit, Bubble, WordPress, Shopify, or anything else. If it runs in a mobile browser, it will work in Natively.

#### **Stable and fast on every device**

Natively is built on the same open-source WebView engine used across the industry, with additional performance improvements applied on top of it. Your app will be stable, fast, and consistent across devices.

#### **A growing library of native features**

Natively comes with a growing library of native features you can enable as your app grows - [Push Notifications](/natively-platform/features/notifications), [In-App Purchases](/natively-platform/features/purchases), [Geolocation](/natively-platform/features/geolocation), [Biometrics & Credentials](/guides/integration/biometrics-and-credentials), [Analytics](/natively-platform/features/analytics), and [more](/guides/integration).

#### **App Store acceptance guarantee**

Natively provides special terms and warranties for acceptance in App Store and Google Play, so you can publish with confidence.

#### **Try before you buy**

Before committing to a paid plan, you can run and test your app in [Preview](/natively-platform/preview) mode - directly on your device.

#### **Support and documentation when you need it**

Natively comes with detailed documentation covering every feature and platform integration. If you get stuck, our support team is available to help you move forward quickly.


# FAQ

General questions

### Does Natively support any website?

Natively supports any website. You can connect it to Natively and build a mobile app without coding. Integration with native features might take some coding, but our [plugins and SDKs](/guides/integration) are very simple and well-documented.

### How much does it cost to build a mobile app?

Check out our [pricing](https://www.buildnatively.com/pricing) page for more details.

But you also will need the Apple developer account ($99/year) or Google Play developer account ($25 once).

### Can I get help with the App Store/Google Play release or Natively setup?

Absolutely. You can handle the setup independently using our [documentation and resources](https://docs.buildnatively.com/guides/testing-and-submitting-your-app).

Alternatively, if you want peace of mind and want to save time, you can hire our team for Paid Assistance. We will review your app's details (metadata, descriptions, and screenshots) to ensure it complies with store policies, provide an optimization checklist if changes are needed, and completely manage the submission process to Apple and Google on your behalf.

Interested in our premium release service? Contact us at <help@buildnatively.com> to get started!

### How do I migrate an existing app to Natively?

If you have a published app that was built using another service or by a different developer, you can migrate it to Natively. Here's how:

1. **Create a new app** in your Natively account.
2. **Upgrade** its plan.
3. **Provide your iOS credentials** as detailed in this [guide](/natively-platform/app-info/ios-build).
4. **Submit this form** with all the required information: <https://tally.so/r/wLZexO>

After submitting the form, we will migrate your app to Natively. This will allow you to upload builds generated by Natively to your TestFlight and Play Console. However, you'll still need to configure any native features you want to use within your app.

### Can I hire an expert to integrate Natively for me? <a href="#can-i-hire-an-expert-to-integrate-natively-for-me" id="can-i-hire-an-expert-to-integrate-natively-for-me"></a>

Yes, of course, you can hire our specialist for an hourly rate to integrate Natively into your website. Contact us: <help@buildnatively.com>

## App functionality and updates

### Will all my website’s functionalities work in the app?

Any functionality associated with your website will work as in Chrome & Safari browsers on a mobile device.

### Some website functionality doesn't work in the app built with Natively

If you experience a feature that functions correctly in mobile browsers like Safari (iOS) or Chrome (Android) but not in the Natively app, please email us the following a code snippet or screenshot, along with a video that compares the feature's behavior in Natively and a mobile browser to <help@buildnatively.com>

### When do I need to rebuild and resubmit my app?

**You DO need to rebuild and resubmit when:**\
**-** You change any settings inside the Natively Dashboard (such as enabling/disabling native features, changing your App URL, or uploading a new App Icon/Launch screen).\
\- You want to apply a new core platform update or bug fix from Natively.

**You DO NOT need a rebuild for:**\
**-** Any changes you make directly inside your web app (fixing a workflow, changing text, updating your database, or editing your web design). These update instantly for your users.

### I don't see a feature that is very important for my application.

We're constantly implementing new native features. If you have any ideas or requirements, feel free to submit your ideas on [this](https://ideas.buildnatively.com) page.\
You can also request a custom feature by emailing us at <help@buildnatively.com>

### Why aren't Natively features working in my web browser?

Natively features are designed to work within the app created with Natively. They won't function in a web browser.

### My iOS Distribution Certificate is expiring soon. Do I need to do anything to keep my app running?

No action is required. Your live app will continue to work for all your users.

An expired Distribution Certificate does not affect apps that are already available on the App Store. Your users can still download, open, and use the app without interruption.

Natively manages these certificates for you. As soon as your current certificate expires, our system will automatically generate a new one the next time you trigger a build.

The only time a certificate matters is when you are signing a new update. Since Natively handles this rotation in the background, your next submission will go through smoothly with the new, valid certificate.

## Account and billing

### Can I change my subscription plan(s)?

You can easily upgrade or downgrade your subscriptions at any time on a prorated basis.

### How do I change the billing period of my subscription?

1. Go to your app's dashboard.
2. In the top right corner, click the "Manage plan" button.
3. Use the toggle to switch between "Monthly Billing" and "Yearly Billing".
4. Click the "Switch to..." button that appears below your plan.
5. Confirm your change.

<figure><img src="/files/8hMJtyeZ899xr0jodfiU" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/OapPPhH0YgR0Ul9CUtII" alt=""><figcaption></figcaption></figure>

### How do I cancel my Natively subscription?

You can easily cancel your subscription through your Natively dashboard.

1. Go to **Payments > Plan Subscriptions:** [**quick link**](https://app.buildnatively.com/ncnp?page=payments)
2. You'll see a list of your active Natively subscriptions.
3. Select the subscription you wish to cancel.
4. Click the **Cancel** button.
5. **Provide us with feedback!** A popup will appear asking you to share your reasons for canceling. We appreciate your honest feedback as it helps us improve Natively for everyone.

Your subscription will then be scheduled to cancel at the end of your current billing period. Your app will retain access to Natively features until that date.

### Will my app continue working if I stop paying my monthly subscription?

Your app will be downgraded to the **Preview** plan when you stop paying your monthly or annual subscription. A blocking banner will appear over your app, preventing normal use until you reactivate your subscription.

<figure><img src="/files/zPWGbUQLqGsiycklazCN" alt=""><figcaption></figcaption></figure>

### I paid for a subscription, why do I still see the 'Subscription inactive' banner?

This can happen due to caching. Sometimes, your device holds onto old information about your subscription status. To fix this, try the following:

* **For App Builds Created After Nov 13, 2025:**

  Locate and tap the Refresh button directly on the banner. This will instantly update your status.
* **For App Builds Created On or Before Nov 13, 2025:**\
  You must clear the app's cache. The most reliable way to do this is to uninstall and reinstall the application. Alternatively, on Android devices, you may try clearing the App Data via your system settings.

If you're still seeing the banner after trying these steps, please contact our support team for further assistance: <help@buildnatively.com>

### How do I upgrade my app to a Lifetime plan?

In your Natively account, click the 'Buy licenses' button. In the popup, select the number of licenses you want to purchase. After confirming, you'll be redirected to a Stripe checkout page for payment.

<figure><img src="/files/0kxh5xMplTVWx2ZiNaiK" alt=""><figcaption></figcaption></figure>

After successful payment, the licenses will be added to your account. Go to the dashboard of the app you want to upgrade. Click 'Manage plan' or 'Upgrade plan'. Scroll down to the bottom and click 'Update to Lifetime'. Then confirm your choice.

<figure><img src="/files/Ky0YaJqunssALIGr8iT9" alt=""><figcaption></figcaption></figure>

If your app has an active subscription, it will be canceled immediately without proration.

### My app is on Lifetime plan. How do I buy more builds?

When your app's build count reaches zero, only the app owner can initiate a new build due to billing responsibilities. If the owner attempts to rebuild, they will be directed to the Stripe checkout page to pay for an additional build. Once the payment is successful, the rebuild process will start automatically.

Here are the steps to order a new build:

1. Log in to Natively using the app owner's account.
2. Select your app from the dashboard.
3. Navigate to the **Publish** section.
4. Choose the platform you wish to rebuild (**Android Build** or **iOS Build**).
5. Click the **Rebuild App** button.
6. Ensure that you set the app version number to be higher than the current version.
7. Click **Rebuild App** again. At this point, you will be redirected to Stripe to pay for the additional build.
8. Complete the payment process on Stripe. Once the payment is successful, the app rebuilding will begin automatically.

### How do I transfer my app to another Natively account?

To transfer your app to another account, please go to the **Settings** section of your app dashboard, then the **Users** tab, and click **Transfer app**. Enter the email address of the user you wish to transfer the app to and click **Yes, transfer**.

Please note that the recipient must have a registered Natively account and will need to confirm the transfer from their Natively account. Also, the current subscription associated with the app will be canceled.

If you prefer to keep the current subscription active, you can invite the user to collaborate on the app instead of transferring ownership. To do this, in the **Users** tab, click **Invite users**, enter their email address, and click **Invite**. This will allow them to see and manage the app within their own Natively account.

### How do I delete my app in Natively?

To delete your app, go to your App Dashboard, select 'Settings,' and scroll down to click the red trash can icon. \
**Important Note:** Deleting your app is a final action. This will immediately cancel your subscription, and no refunds will be issued for the remaining subscription period.

### How do I request a refund from Natively?

If you're unsatisfied with Natively, you can request a refund before 3 days after your purchase. To initiate the process, contact our support chat or email us at <help@buildnatively.com> with the following details:

* Reasons for requesting a cancellation or refund
* Suggestions on how we can improve
* The email associated with your Natively account


# Create Your First App

{% embed url="<https://youtu.be/ZtI6vr4Psng>" %}


# Other Platform Integrations

We’re so excited to announce support for Lovable.dev, Base44 andReplit


# Lovable.dev

We’re so excited to announce support for Lovable.dev! Now you can take the app you’ve built on Lovable.dev and turn it into a professional mobile app for the Apple App Store and Google Play Store.

This guide is designed for beginners. Even if you’ve never published an app before, we’ll walk you through every step.

***

### 1. What You’ll Need Before You Start

Before diving in, here are the things you’ll need:

* A **Lovable.dev app** (already created and live on the web).
* A **BuildNatively account** (sign up at buildnatively.com).
* A **Google Play Developer account**&#x20;
* An **Apple Developer account**&#x20;

Don’t worry — you don’t need an Apple computer or technical setup. BuildNatively handles the heavy lifting.

***

### 2. Step 1: Prepare Your Lovable.dev App

1. Log in to your **Lovable.dev** dashboard.
2. Make sure your app is published and accessible through a link (e.g., `https://myapp.lovable.dev`).
   * This link is important because BuildNatively uses it to display your app inside the mobile app.
3. Double-check that your app works well on **mobile browsers** (Safari, Chrome).
   * If it looks good in a mobile browser, it will look good inside your mobile app.

👉 **Tip:** If your app requires users to log in, test the login flow to make sure it’s smooth.

***

### 3. Step 2: Create Your Mobile App in BuildNatively

1. Log in to your **BuildNatively** dashboard.
2. Click **“Create New App”**.
3. Enter your **App Name** (this is what users will see under your app icon).
4. Paste your **Lovable.dev app URL** into the “App URL” field.
5. Choose your **platforms**: iOS, Android, or both.
6. Save your app — you now have a mobile app draft!

👉 **What’s happening here?**\
BuildNatively is wrapping your Lovable.dev app inside a native shell. This allows it to run on iOS and Android, while also unlocking native features like push notifications, geolocation, and in-app purchases.

***

### 4. Step 3: Customize Your Mobile App

Now that your app is created, it’s time to make it yours.

#### App Icon

* Upload a **1024x1024 PNG image** (no transparent background).
* This will appear on your users’ phones.

#### Splash Screen (Launch Screen)

* Upload a **2048x2048 PNG image**.
* This is the screen that shows while your app loads.

#### Style & Colors

* Choose your background color and accent colors.
* Customize your navigation (Bottom Bar, Top Bar, or no bar).

#### Native Features

From your dashboard, you can turn on features like:

* **Push Notifications** (stay in touch with users).
* **Geolocation** (for maps or location-based services).
* **Social Login** (Google, Apple, or Facebook login).
* **In-App Purchases** (sell digital goods or subscriptions).
* **Camera & File Uploads** (users can take pictures or upload files).

👉 **Tip:** You can enable features now or later. Start simple if you’re new.

***

### 5. Step 4: Preview Your App on Your Phone

Before publishing, you’ll want to see how it looks.

1. Download the **BuildNatively Preview App** from the App Store or Google Play.
2. Log in with your BuildNatively account.
3. Select your app, and preview it live on your phone.

👉 **Important Note:** Some advanced features (like push notifications or in-app purchases) won’t work in the preview app. They will only work once your app is fully built.

***

### 6. Step 5: Build & Publish Your App

When you’re happy with your app, it’s time to get it into the app stores.

#### iOS (Apple App Store)

1. Connect your **Apple Developer Account** inside BuildNatively.
2. BuildNatively will create your iOS app build.
3. Your app will be delivered to **TestFlight** for testing.
4. Once approved, you can submit to the **App Store**.

#### Android (Google Play Store)

1. Connect your **Google Play Developer Account**.
2. BuildNatively will generate an **APK/AAB file**.
3. Upload this file to your **Google Play Console**.
4. Publish your app to the Play Store.

👉 **Good News:** You do **not** need a Mac or Xcode. BuildNatively handles all the technical parts for you.

***

### 7. Step 6: Keep Improving Your App

Once your app is live, you can:

* Send **push notifications** to keep users engaged.
* Update your app’s design or settings in BuildNatively.
* Rebuild and publish updates anytime (takes about 10 minutes).

👉 **Pro Tip:** You don’t need to rebuild for every small change on Lovable.dev. Any update you make to your Lovable.dev app will instantly reflect in your mobile app. You only need a rebuild if you change **native features**.

***

### 8. Example Scenarios

Here are some ways Lovable.dev + BuildNatively can work together:

* **Education App:** Create a learning portal in Lovable.dev and send students push notifications for new lessons.
* **E-commerce App:** Showcase products and enable purchases with native payment flows.
* **Community App:** Allow members to log in with Google or Apple, and send updates with notifications.

***

### 9. Frequently Asked Questions

**Q: Do I need to know how to code?**\
A: Not at all! Both Lovable.dev and BuildNatively are designed for non-technical users.

**Q: How long does it take to publish?**\
A: The build process takes about 10 minutes. Apple and Google’s review times vary — usually 1-3 days.

**Q: Will my app work the same as my Lovable.dev site?**\
A: Yes — it’s the same app, just in a mobile wrapper with extra features.

**Q: Can I update my app after publishing?**\
A: Yes! Update your Lovable.dev app anytime. If you change native features, just rebuild in BuildNatively and resubmit.

***

### 10. You’re All Set! 🎉

That’s it — you’ve taken your **Lovable.dev app** and turned it into a real mobile app using **BuildNatively**. Now your users can find and install your app on the **App Store** and **Google Play Store**.

If you run into any issues, don’t worry — our support team is always here to help at **<help@buildnatively.com>**


# Quick Start Checklist

### ✅ Step 1: Prepare Your Lovable.dev App

* Make sure your app is published with a **secure HTTPS link** (e.g., `https://myapp.lovable.dev`).
* Test your app in a **mobile browser** (Safari/Chrome) to check layouts.
* Ensure each important page has a **unique URL** (e.g., `/login`, `/profile`, `/checkout`).

***

### ✅ Step 2: Connect to BuildNatively

* Log in to your **BuildNatively dashboard**.
* Click **Create New App**.
* Enter your app name and paste your Lovable.dev **URL**.
* Save — your app is now inside BuildNatively.

***

### ✅ Step 3: Customize Your App

* Upload your **App Icon** (1024x1024 PNG).
* Upload your **Splash Screen** (2048x2048 PNG).
* Adjust style, colors, and navigation (Bottom Bar, Top Bar, etc.).
* Decide which **native features** you need (push notifications, social login, payments, etc.).

***

### ✅ Step 4: Test on Your Phone

* Download the **BuildNatively Preview App** (iOS/Android).
* Log in and preview your app.
* Remember: some features (push notifications, in-app purchases) won’t work in preview, only in the final build.

***

### ✅ Step 5: Publish Your App

* Connect your **Apple Developer Account**&#x20;
* Connect your **Google Play Developer Account**.
* Build your app in BuildNatively (takes about 10 minutes).
* Submit to the **App Store** and **Google Play Store**.

***

### 🚀 Pro Tips for Success

* Mark **external links** (Stripe, YouTube, Calendly, etc.) in BuildNatively so they open in the browser.
* For **digital products/subscriptions**, use **In-App Purchases** instead of Stripe.
* Fill out **permission descriptions** (e.g., “We use your location to show nearby events”).
* Use **push notifications + deeplinks** to guide users directly to content.

***

That’s it! 🎉 You’re ready to take your Lovable.dev app live as a real mobile app.

👉 If you get stuck, check the **detailed integration guide** or email us at **<help@buildnatively.com>**


# Integration

In addition to creating your app in BuildNatively, there are a few things you’ll need to set up inside your **Lovable.dev project**. These adjustments make sure your app behaves properly inside a mobile app environment and that all features (like logins, links, and payments) work as expected.

#### 1. Make Sure Your Lovable.dev App is Published Securely

* Your app must be published with a **secure HTTPS link** (e.g., `https://myapp.lovable.dev`).
* iOS and Android **do not allow** apps to load insecure (HTTP) websites.
* If you’re testing with a temporary link, switch to the secure version before connecting it to BuildNatively.

***

#### 2. Organize Internal and External Links

* **Internal Links (inside your app):** These are links to your app’s own pages (like `/dashboard`, `/shop`, or `/profile`).
  * These should stay inside the app when clicked.
  * No changes are needed here if you’re using Lovable’s built-in navigation.
* **External Links (outside your app):** These include links to websites like Stripe Checkout, Calendly, YouTube, or help docs.
  * If you don’t configure these, users may get “stuck” inside your app or see errors.
  * In BuildNatively, you’ll need to **mark these links as external**, so they open in the user’s phone browser instead of inside the app.

👉 **Action for You:** Review your Lovable app and make a list of any links that point outside your own site. These will need to be added as “external” in BuildNatively’s settings.

***

#### 3. Adjust Login & Authentication Settings

If your app has login or signup features, here’s what you need to check:

* **Test standard email/password login** in your Lovable app on mobile (Safari/Chrome). If it works in the browser, it will work inside your app.
* If you’re using **Social Login (Google, Apple, Facebook)**:
  * You must enable **Universal Links / Deeplinks** in BuildNatively.
  * Lovable.dev should redirect back to your app correctly after authentication.
  * Without this step, logins may redirect users to a browser instead of back into your app.

👉 **Action for You:** If you use social login, plan to configure Universal Links in BuildNatively as part of your setup.

***

#### 4. Optimize the Layout for Mobile Screens

* Even though BuildNatively wraps your Lovable app as-is, the design still comes from Lovable.dev.
* That means you need to make sure your Lovable app looks good on small screens:
  * Avoid very wide tables, large popups, or elements that don’t resize properly.
  * Test important flows (like checkout, signup, and dashboard pages) on a real phone browser before connecting it to BuildNatively.

👉 **Action for You:** Open your Lovable app on your own phone and go through it like a user would. If anything looks too big or hard to use, adjust it in Lovable.dev before publishing.

***

#### 5. Prepare for Push Notifications & Deeplinks

If you plan to send push notifications (for example, “New message received” or “Lesson unlocked”), you’ll want them to open the right screen in your app when tapped.

* BuildNatively supports **deeplinks**, which means you can link directly to a page inside your Lovable app (e.g., `/messages/123`).
* To make this work, your Lovable app must be able to handle these URLs properly.

👉 **Action for You:** Make sure each important page in your app has a unique, shareable URL (not just hidden behind popups). This way, notifications can link directly to it.

***

#### 6. Payments & Checkout Pages

If you’re using Stripe or another payment system inside Lovable.dev:

* Test your checkout flow on mobile browsers first.
* If your checkout redirects users to an **external payment page** (like Stripe Checkout), make sure that page is marked as an **external link** in BuildNatively.
* Otherwise, users may not be able to complete their payment inside the app.

***

#### Summary of What You Need to Do in Lovable.dev

Before wrapping your Lovable app with BuildNatively, make sure to:

1. Publish it with a **secure HTTPS link**.
2. Separate **internal vs. external links** and prepare to configure them.
3. Test your **login/signup flows**, especially social login.
4. Adjust layouts so the app looks good on **small mobile screens**.
5. Make sure important pages have unique URLs for **deeplinks and notifications**.
6. Test **checkout flows** if you’re selling products or services.

***

👉 Once these steps are done in Lovable.dev, your app will be fully ready to integrate with BuildNatively and take advantage of native features like push notifications, social login, and app store publishing.

***


# Troubleshooting Guide

Here’s a detailed list of the **most common issues** you may encounter when turning your Lovable.dev app into a mobile app with BuildNatively, along with their solutions.

***

### 1. Login & Authentication Issues

#### 1.1 My login opens in Safari/Chrome instead of inside the app

* **Why it happens:** Social login (Google, Apple, or Facebook) requires special setup to return users to the app instead of a browser.
* **How to fix:**
  1. In BuildNatively, enable **Universal Links (Deeplinks)** under the Features section.
  2. Make sure your Lovable.dev app redirects users back to your domain after login.
  3. Rebuild your app after enabling Universal Links.

***

#### 1.2 My Google/Apple login doesn’t work at all

* **Why it happens:** Social Auth requires both your Lovable.dev settings and BuildNatively’s Deeplinks to be configured correctly.
* **How to fix:**
  * Confirm that **Social Auth** is turned on in BuildNatively.
  * Enable **Associated Domains** for iOS in your Apple Developer account.
  * Verify your redirect URL matches your Lovable.dev app’s domain.

***

#### 1.3 Email/password login works in Lovable.dev but fails in the app

* **Why it happens:** Sometimes authentication cookies don’t carry over correctly in the mobile app.
* **How to fix:**
  * Check that your Lovable.dev app is running under **HTTPS** (not HTTP).
  * Test login in a mobile browser first. If it works there, it should work in the app.

***

### 2. Payments & Checkout Issues

#### 2.1 Stripe or checkout pages don’t load inside the app

* **Why it happens:** Payment providers like Stripe or PayPal open in secure external pages. If the app tries to load them internally, they may break.
* **How to fix:**
  * Mark your checkout links as **external links** in the BuildNatively dashboard.
  * This ensures they open in the user’s default browser, where payments complete correctly.

***

#### 2.2 Apple/Google reject my app because of Stripe payments

* **Why it happens:** For digital goods/services, Apple and Google require **In-App Purchases (IAP)**. Stripe can only be used for physical products, real-world services, or P2P payments.
* **How to fix:**
  * If you sell **digital goods/subscriptions**, enable **In-App Purchases** in BuildNatively (powered by RevenueCat).
  * If you sell **physical goods/services**, you may keep using Stripe.

***

### 3. Push Notifications & Deeplinks

#### 3.1 Notifications show, but nothing happens when I tap them

* **Why it happens:** Notifications need a **deeplink** to know which screen to open.
* **How to fix:**
  * In BuildNatively, configure notifications with a specific page URL (e.g., `/messages/123`).
  * Ensure your Lovable.dev app has that page accessible.

***

#### 3.2 Notifications send me to the wrong page

* **Why it happens:** The deeplink used may not match your Lovable.dev app’s routing.
* **How to fix:**
  * Double-check that the link in your notification exactly matches the structure in your Lovable.dev app.

***

#### 3.3 Notifications don’t arrive at all

* **Why it happens:** Push notifications require setup with **OneSignal** or **Firebase**.
* **How to fix:**
  * Check that you’ve uploaded the correct config files in BuildNatively.
  * Rebuild your app after enabling notifications.

***

### 4. Layout & Display Issues

#### 4.1 Some pages look broken or cut off on mobile

* **Why it happens:** Your Lovable.dev design may not be fully optimized for small screens.
* **How to fix:**
  * Open your Lovable.dev app on your phone and test every key page.
  * Adjust layouts in Lovable.dev (resize elements, stack content vertically).

***

#### 4.2 My app looks different in the BuildNatively Preview App

* **Why it happens:** The Preview App does not support all native features.
* **How to fix:**
  * Use it mainly to check **design, navigation, and content flow**.
  * Do a full build to test native features like notifications, in-app purchases, or deeplinks.

***

#### 4.3 Scroll or zoom issues on forms

* **Why it happens:** iOS automatically zooms in on text fields with small font sizes.
* **How to fix:**
  * In your Lovable.dev design, increase input font sizes to at least **16px**.

***

### 5. Links & Navigation

#### 5.1 External links (YouTube, Calendly, Help Docs) don’t work

* **Why it happens:** BuildNatively assumes all links are internal unless marked otherwise.
* **How to fix:**
  * Add these links as **external** in the BuildNatively dashboard so they open in the browser.

***

#### 5.2 Clicking a link sometimes reloads the entire app

* **Why it happens:** If the link structure isn’t set correctly, the app may think you’re leaving the app.
* **How to fix:**
  * Ensure your internal links all use the **same root domain** as your Lovable.dev app.

***

### 6. Publishing Issues

#### 6.1 Apple rejected my app for missing permission descriptions

* **Why it happens:** Apple requires you to explain why your app asks for permissions (camera, location, microphone, etc.).
* **How to fix:**
  * In BuildNatively, fill out the **Permission Description** fields under each enabled feature. Example: *“We use your location to show nearby events.”*

***

#### 6.2 My app was rejected for using the wrong payment system

* **Why it happens:** Apple and Google require **In-App Purchases** for digital goods.
* **How to fix:**
  * If you sell digital items, enable RevenueCat integration.
  * If you sell physical goods, Stripe is fine.

***

#### 6.3 Google flagged my app for unsupported memory pages (NDK)

* **Why it happens:** Google is updating requirements for Android builds.
* **How to fix:**
  * No action needed now — BuildNatively will handle this automatically in future updates.

***

### 7. Other Common Issues

#### 7.1 My app loads very slowly

* **Why it happens:** Your Lovable.dev app may have heavy images or scripts.
* **How to fix:**
  * Optimize images and videos inside Lovable.dev.
  * Use fewer large plugins or animations.

***

#### 7.2 My changes in Lovable.dev don’t appear in the app

* **Why it happens:** You may be looking at a cached version of your app.
* **How to fix:**
  * Close and reopen your app.
  * If you changed something in **BuildNatively** (like features, style, or permissions), you’ll need to **rebuild**.

***

#### 7.3 The app works in the browser but breaks inside BuildNatively

* **Why it happens:** Some features behave differently inside mobile apps (compared to browsers).
* **How to fix:**
  * Check if the feature is listed as **unsupported in Preview**.
  * Contact BuildNatively support if it persists after a full build.

***

## Quick Checklist Before Submitting to App Stores

* ✅ HTTPS link is used.
* ✅ Internal/external links are set correctly.
* ✅ Login/signup flows tested (with Universal Links if needed).
* ✅ Layout looks good on small screens.
* ✅ Payment flow matches Apple/Google rules.
* ✅ Permission descriptions are filled out.
* ✅ Push notifications and deeplinks tested.


# Base44

Good news! If you built your app with Base44, you can convert it into real App Store and Google Play apps with BuildNatively — no coding needed.

This guide walks you step-by-step, explains what you need to change inside your Base44 project, and shows how to configure BuildNatively so everything “just works.”

***

### What You’ll Need Before You Start

* A Base44 web app that opens with a secure HTTPS link.
* A BuildNatively account.
* Apple Developer and Google Play Developer accounts (for publishing).

Tip: If your app works in a mobile browser like Safari or Chrome, it will work in a native app with BuildNatively.

***

### Part 1 — Prepare Your Base44 App (Important!)

These simple adjustments inside Base44 will save you time later.

#### 1) Use a Secure HTTPS URL

All content must load over HTTPS. If anything loads over plain HTTP, iOS and Android can block it. Publish your Base44 app to an HTTPS URL before connecting it to BuildNatively.

#### 2) Keep One Primary Domain

Use a single primary domain for your app. Avoid switching between multiple subdomains for key screens (example: moving from app.example.com to accounts.example.com during login), unless you plan to mark those as external in BuildNatively.

#### 3) Make Every Important Screen Reachable by a Stable URL

Pages like Login, Signup, Profile, Checkout, Messages, and Orders should each have their own clean, shareable URL (for example, `/login`, `/profile`, `/orders/123`). This makes deep links and push-to-page actions work reliably.

#### 4) Prefer Pages Over Popups for Critical Flows

If login, payment, or onboarding are only in popups or modals with no URL change, deep links and notifications can’t open them. Create real pages with routes for these flows.

#### 5) Test the App on a Real Phone Browser

Open your Base44 app on an iPhone and an Android phone. Check that text fits on small screens, buttons are easy to tap, forms scroll properly, and video/images load quickly. Fix any layout issues in Base44 first — BuildNatively will show the same web content.

#### 6) Decide How You’ll Handle Login

* If you use standard email/password and it works in a mobile browser, it will work in the app.
* If you plan to add Google, Apple, or Facebook login, you’ll need to enable Social Auth and Universal Links in BuildNatively later.

#### 7) Plan External Links

List any links that go outside your Base44 domain (payments, YouTube, Calendly, Help Center, external docs). You will mark these as external in BuildNatively so they open in the phone’s browser and don’t get “stuck” inside your app.

***

### Part 2 — Create Your Mobile App in BuildNatively

1. Create a new app. Enter your app name and paste your Base44 HTTPS URL.
2. Upload your app icon. Use a 1024×1024 PNG without transparency.
3. Set your loading and error screens. Use 2048×2048 PNG images. These show while your app loads or if the network drops.
4. Choose navigation. If you want a bottom tab bar, configure tabs and their URLs now. You can also show or hide it programmatically later.
5. Define internal and external links. Add your own domain(s) as internal so they stay inside the app. Add any third-party sites (payments, video, scheduling) as external so they open in the phone’s browser.

***

### Part 3 — Enable Native Features (Optional but Powerful)

You can enable these at any time. When you change native features, you’ll need to rebuild your app.

#### Push Notifications

* Choose OneSignal or Firebase.
* Store each user’s notification identifier in your database so you can target them later.
* For “tap to open a page” behavior, include a deep link to the exact screen (for example, `/orders/123`).

#### Deep Links (Universal Links / App Links)

Deep links let a tap open a specific screen in your app. For new projects, use Native deep links or Branch.

#### Social Auth (Google, Apple, Facebook)

Social login requires working deep links so the user is returned to your app after authorization. Turn on Social Auth and Universal Links, set your app domain, and follow the setup steps. Apple Sign In also needs to be enabled for your iOS bundle ID.

#### In-App Purchases and Subscriptions

If you sell digital content or subscriptions, enable Purchases via RevenueCat inside BuildNatively. Follow the setup guide to connect your products and entitlement logic.

Other popular features include geolocation, native camera, QR/barcode scanner, contacts, haptics, and analytics. You can add these later as your app grows.

***

### Part 4 — Preview and Test

Use the Natively Preview app to see your Base44 app running on a phone before building. This is perfect for checking layout, navigation, and content.\
Note: Some native features do not work in Preview (for example, deep links, Social Auth, Admob, background location, in-app purchases). Build a test version when you’re ready to validate those.

***

### Part 5 — Build and Publish

When your app looks good in Preview:

1. Build for iOS and Android in your BuildNatively dashboard.
2. iOS builds are delivered to TestFlight for testing, then submitted for App Store review.
3. Android builds are generated for upload in Google Play Console.
4. If you enable or change native features later, rebuild to ship the update.

***

### Base44-Specific Best Practices

* Keep forms and key actions on full pages, not just popups, so deep links and notifications can open them.
* Make input text at least 16px to prevent iOS auto-zoom on fields.
* Compress large images and videos to speed up mobile startup.
* If you add third-party tools in Base44 (chat widgets, schedulers, external docs), list their domains and mark them external in BuildNatively.

***

### Troubleshooting: Quick Fixes

**Login opens in the browser and doesn’t return to the app**\
Enable Universal Links, verify the app domain, and rebuild. Social Auth relies on deep links to return users to your app after authorization.

**Stripe or other checkout pages don’t work inside the app**\
Mark checkout pages as external so they open in the phone’s browser.

**Notification arrives but tapping it does nothing**\
Include a deep link in the notification payload to the exact screen (for example, a specific order or message thread). Ensure that screen has a stable URL in your Base44 app.

**Some native features work in the built app but not in Preview**\
That’s expected. Use Preview for layout checks; build a test version to validate deep links, Social Auth, in-app purchases, background features, and Admob.

**My changes in Base44 aren’t showing in the app**\
Close and reopen the app to clear cache. If you changed native settings or appearance (icon, splash, permissions), rebuild to apply them.

***

### Quick Start Checklist

1. Base44 app is live over HTTPS and looks good on a phone.
2. Each important screen has its own URL; critical flows are pages, not only popups.
3. In BuildNatively: icon (1024×1024 PNG), splash/error images (2048×2048 PNG).
4. Internal vs external links are configured.
5. Optional: enable Push and Deep Links; if using Social Auth, set up Universal Links.
6. Preview the app; then build for iOS and Android and publish.

***

### Why This Works Well for Base44 Creators

Base44 lets you go from idea to working web app fast, without code. BuildNatively takes that same app and adds the native layer — push notifications, deep links, secure app store delivery, and more — so your users can install it from the App Store and Google Play like any other app.


# Quick Start Checklist

### Step 1: Prepare Your Base44 App

Before connecting to BuildNatively, make sure your app is ready.

* Your Base44 app is published with a **secure HTTPS URL** (required by iOS/Android).
* Your app uses **one primary domain** for all internal pages.
* Each key page (login, signup, profile, checkout, dashboard) has its own **unique URL**.
* Avoid popups/modals for critical actions (login, payments). Use full pages where possible.
* Test your app on a real phone browser:
  * Check that text, buttons, and forms look good on small screens.
  * Fix anything that looks cut off, too wide, or hard to tap.
* If you use Social Login (Google, Apple, Facebook), note that you’ll need to enable **Universal Links** in BuildNatively.
* Make a list of **external links** (Stripe, YouTube, Calendly, external docs). These will be configured as external later.

***

### ✅ Step 2: Set Up Your Mobile App in BuildNatively

* Log in to your BuildNatively dashboard.
* Click **Create New App**.
* Enter your app’s name.
* Paste your Base44 HTTPS URL.
* Save the app.

Now your Base44 app is linked to BuildNatively!

***

### ✅ Step 3: Customize Your App

* **App Icon:** Upload a **1024×1024 PNG** (no transparent background).
* **Splash Screen:** Upload a **2048×2048 PNG** (shown while the app loads).
* **Error Screen:** Upload another **2048×2048 PNG** (shown if network fails).
* **Navigation:** Choose between Bottom Bar, Top Bar, or no navigation bar. Add tab links if using a bottom bar.
* **Colors & Style:** Adjust background, accent colors, and overall appearance.
* **Permissions:** If your app uses location, camera, microphone, etc., write clear permission descriptions (example: *“We use your camera to let you upload a profile picture”*).

***

### ✅ Step 4: Configure Links

* Add your Base44 domain as **Internal** so it stays inside the app.
* Add all third-party sites (Stripe, Calendly, YouTube, external help docs) as **External** so they open in the user’s phone browser.
* This prevents users from getting “stuck” when leaving your Base44 site.

***

### ✅ Step 5: Enable Native Features (Optional, But Powerful)

* **Push Notifications:** Set up OneSignal or Firebase. Use deep links to send users to exact screens when they tap a notification.
* **Deep Links (Universal Links):** Required for Social Login (Google, Apple, Facebook) and helpful for opening specific screens from links.
* **In-App Purchases:** Use if selling digital content/subscriptions (via RevenueCat).
* **Other Features:** Enable extras like geolocation, camera, QR scanner, contacts, haptics, or analytics depending on your needs.

### ✅ Step 6: Test Your App

* Download the **BuildNatively Preview App** on your phone.
* Log in and preview your Base44 app.
* Check layout, navigation, and page flow.
* Remember: some features (push notifications, Social Auth, deep links, in-app purchases) will only work after a full build.

***

### ✅ Step 7: Build & Publish

* In your BuildNatively dashboard, create your iOS and Android builds.
* For iOS:
  * Connect your Apple Developer account.
  * Test in TestFlight.
  * Submit to the App Store.
* For Android:
  * Connect your Google Play Developer account.
  * Upload the APK/AAB to Google Play Console.
  * Publish to the Play Store.

***

### ✅ Step 8: After Publishing

* Update your Base44 app anytime — changes appear instantly in your mobile app (no rebuild needed).
* If you change native features (push notifications, payments, deep links, navigation), rebuild in BuildNatively.
* Use push notifications and deep links to keep users engaged.
* Track usage with analytics if enabled.

***

## Pro Tips for Success

* Keep your app **lightweight** — optimize images and videos in Base44 for faster load times.
* Use **mobile-first design** in Base44 to make sure everything looks good on phones.
* Always mark **checkout/payment links as external** to avoid errors.
* Write clear **permission descriptions** to prevent App Store rejections.
* Plan to test **login, payments, and notifications** on real devices before submitting


# Integration

o make sure your Base44 app works seamlessly inside your mobile app, you’ll need to prepare a few things directly in your Base44 project. These steps are essential for proper login flows, payments, navigation, and deep linking.

#### 1. Publish Your Base44 App with HTTPS

* Your Base44 app must be published using a secure **https\://** link.
* iOS and Android do not allow insecure (http\://) content inside mobile apps.
* If your app isn’t yet on HTTPS, update your Base44 publishing settings before moving forward.

***

#### 2. Use One Primary Domain

* Keep your app on a single main domain.
* If parts of your app (like login or payments) jump to a different subdomain, it can break navigation inside the mobile app.
* Example: If your main app runs on `app.example.com`, try to also run login and checkout flows there instead of switching to `accounts.example.com`.
* If you cannot avoid external domains, you must mark them as **external links** in BuildNatively so they open in the user’s browser.

***

#### 3. Make Every Key Screen Accessible by a URL

* Each important page in your Base44 app should have its own **stable, shareable URL**.
* Examples:
  * `/login` for the login page
  * `/profile` for the user profile
  * `/orders/123` for a specific order
* This is critical for:
  * **Deep Links** (so notifications or external links open the right page)
  * **Social Login** (so users are returned to the right place after authentication)
  * **User navigation** (so users don’t lose their place inside the app)

***

#### 4. Avoid Popups for Critical Actions

* Some builders allow you to make login, signup, or checkout as popups instead of full pages.
* Popups don’t create a unique URL — which means deep links, notifications, and re-entry flows won’t work.
* Instead, make these actions full pages with their own URLs (e.g., `/login`, `/checkout`).

***

#### 5. Test Your Base44 App on a Phone Browser

* Open your Base44 app in Safari (iPhone) and Chrome (Android).
* Check:
  * Do text and buttons fit on the screen?
  * Are forms easy to use without zooming?
  * Do pages scroll properly?
  * Are images and videos loading quickly?
* Fix anything that doesn’t look good on mobile, because BuildNatively will show the same content.

***

#### 6. Plan for Login & Authentication

* **Email/Password Login:** If this works in a phone browser, it will work inside your mobile app.
* **Social Login (Google, Apple, Facebook):**
  * These require **deep links (Universal Links)** to return the user to your app after login.
  * You’ll need to configure this later in BuildNatively.
  * Make sure your Base44 login redirects back to your main app domain.

***

#### 7. Review All External Links

* Identify any links in your Base44 app that go outside your own domain, such as:
  * Stripe checkout
  * PayPal payments
  * YouTube videos
  * Calendly booking pages
  * External help docs
* These must be marked as **external links** in BuildNatively. Otherwise, they may break or trap users inside the app.

***

#### 8. Prepare Checkout & Payment Pages

* If you’re selling products or services, test your checkout flow in a mobile browser.
* If checkout uses an external page (like Stripe Checkout), mark it as external in BuildNatively.
* If you sell **digital content or subscriptions**, plan to use **In-App Purchases (RevenueCat)**, because Apple and Google require this instead of Stripe.

***

#### 9. Optimize for Mobile Performance

* Compress large images and videos to reduce load times.
* Avoid heavy scripts that slow down mobile.
* Keep layouts simple and mobile-first.

***

#### 10. Set Clear Page Routes for Notifications & Deep Links

* Push notifications and deeplinks rely on clean URLs.
* Example:
  * A notification for “New Lesson Available” should link to `/lessons/456`.
  * A notification for “New Message” should link to `/messages/123`.
* Make sure these URLs exist in your Base44 project and open the correct content.

***

#### Summary of Base44 Integration Steps

Before connecting to BuildNatively, make sure you have:

1. HTTPS enabled for your Base44 app.
2. A single primary domain for your app.
3. Stable URLs for all important screens.
4. Full pages (not popups) for login, signup, and checkout.
5. A mobile-optimized layout tested on real devices.
6. A plan for Social Login (with deep links).
7. A list of external links (Stripe, YouTube, Calendly, etc.).
8. Checkout flows tested and marked as external if needed.
9. Images and videos optimized for mobile.
10. Clean routes prepared for notifications and deeplinks.


# Troubleshooting Guide

Here’s a detailed list of the **most common issues** you may encounter when turning your Base44 app into a mobile app with BuildNatively, along with their solutions.

***

### 1. Login & Authentication Issues

#### 1.1 My login opens in Safari/Chrome instead of inside the app

* **Why it happens:** Social login (Google, Apple, or Facebook) requires deep links (Universal Links/App Links) to send users back to the app after login.
* **How to fix:**
  1. Enable **Deep Links** in BuildNatively.
  2. Turn on **Social Auth** in the Features section.
  3. Rebuild your app.
* **Tip:** Make sure your Base44 login flow redirects users back to your **main app domain**.

***

#### 1.2 My email/password login works in Base44 but fails in the app

* **Why it happens:** Sessions can break if cookies or redirects move across different domains.
* **How to fix:**
  * Check that your Base44 app is fully on **HTTPS**.
  * Keep login and authentication on the same domain.
  * Avoid unnecessary redirects to other subdomains.

***

#### 1.3 I get stuck on a blank page after login

* **Why it happens:** Post-login redirects may point to a popup/modal instead of a full page with a URL.
* **How to fix:**
  * In Base44, make login and post-login screens **full pages with stable URLs** (e.g., `/dashboard`).
  * Update your redirect settings accordingly.

***

### 2. Payments & Checkout Issues

#### 2.1 Stripe or checkout pages don’t load properly

* **Why it happens:** Many payment providers require opening in the device’s browser for security.
* **How to fix:**
  * Mark your checkout pages as **external links** in BuildNatively.
  * This ensures they open in Safari/Chrome and complete properly.

***

#### 2.2 Apple/Google rejected my app because of payments

* **Why it happens:** App Stores require **In-App Purchases** for selling digital goods or subscriptions.
* **How to fix:**
  * Use BuildNatively’s **In-App Purchases** feature (via RevenueCat) for digital products.
  * Use Stripe/PayPal only for **physical goods** or **real-world services**.

***

### 3. Push Notifications & Deeplinks

#### 3.1 Notifications arrive but do nothing when tapped

* **Why it happens:** Notifications need a **deeplink** to open the correct screen.
* **How to fix:**
  * Include a unique URL in your notification (e.g., `/orders/123`).
  * Make sure that URL exists in Base44 and loads the correct page.

***

#### 3.2 Notifications open the wrong page

* **Why it happens:** The deeplink doesn’t match your Base44 routing.
* **How to fix:**
  * Double-check that the deeplink matches your Base44 page structure exactly.

***

#### 3.3 Notifications don’t arrive at all

* **Why it happens:** Push setup may be incomplete.
* **How to fix:**
  * Confirm that push notifications are enabled in BuildNatively.
  * Verify that the correct configuration files (OneSignal/Firebase) are uploaded.
  * Rebuild your app after enabling push.

***

### 4. Layout & Display Issues

#### 4.1 Some pages look broken or cut off on small screens

* **Why it happens:** The Base44 layout isn’t optimized for mobile.
* **How to fix:**
  * Test your Base44 app on a real phone browser.
  * Adjust layouts in Base44 (stack elements vertically, avoid wide tables).

***

#### 4.2 iOS zooms in too much on forms

* **Why it happens:** iOS automatically zooms when input text is too small.
* **How to fix:**
  * In Base44, set input text sizes to **at least 16px**.

***

#### 4.3 Scrolling feels broken or clunky

* **Why it happens:** Nested scroll areas or heavy animations can cause problems.
* **How to fix:**
  * Avoid multiple scrollable sections on the same page.
  * Reduce heavy scripts or animations.

***

### 5. Links & Navigation

#### 5.1 External links don’t work (YouTube, Calendly, Help Docs)

* **Why it happens:** BuildNatively treats all links as internal by default.
* **How to fix:**
  * Mark these links as **external** in BuildNatively so they open in the device’s browser.

***

#### 5.2 Clicking a link reloads the entire app

* **Why it happens:** The link may point to a different domain or subdomain.
* **How to fix:**
  * Ensure internal links use your app’s **primary domain**.

***

#### 5.3 My bottom tab bar doesn’t work correctly

* **Why it happens:** Each tab needs a valid internal URL.
* **How to fix:**
  * Check that each tab links to a real Base44 page with a stable URL.

***

### 6. Media, Camera & Files

#### 6.1 Camera or microphone don’t work

* **Why it happens:** Permissions aren’t enabled.
* **How to fix:**
  * In BuildNatively, enable **Camera/Microphone**.
  * Add clear **permission descriptions** (e.g., “We use your camera to upload a profile picture”).
  * Rebuild your app.

***

#### 6.2 File uploads fail inside the app

* **Why it happens:** Uploads may use insecure endpoints or unstable routes.
* **How to fix:**
  * Make sure your upload endpoint uses **HTTPS**.
  * Test uploads on a full Base44 page, not a popup.

***

#### 6.3 Images and videos make my app slow

* **Why it happens:** Large media files increase load time.
* **How to fix:**
  * Compress images and videos in Base44 before uploading.

***

### 7. Location & Permissions

#### 7.1 Location services don’t work

* **Why it happens:** Permissions are missing.
* **How to fix:**
  * Enable **Geolocation** in BuildNatively.
  * Add a clear permission description (e.g., “We use your location to show nearby events”).
  * Rebuild and test again.

***

#### 7.2 Apple rejected my app for permissions

* **Why it happens:** Apple requires descriptions for each permission you request.
* **How to fix:**
  * In BuildNatively, write clear, user-friendly permission explanations.
  * Example: “We use your contacts to help you invite friends to the app.”

***

### 8. Publishing Issues

#### 8.1 Apple rejected my app for being “just a website”

* **Why it happens:** Apple requires apps to have native functionality.
* **How to fix:**
  * Enable at least one **native feature** (push notifications, deep links, native share, camera, etc.).
  * Resubmit with these features enabled.

***

#### 8.2 My app icon or splash screen didn’t update

* **Why it happens:** Old assets may be cached.
* **How to fix:**
  * Upload the new assets in BuildNatively.
  * Rebuild your app.
  * Uninstall the old version from your phone before testing again.

***

#### 8.3 My app loads slowly on startup

* **Why it happens:** The Base44 homepage is too heavy.
* **How to fix:**
  * Optimize images and videos.
  * Remove or delay loading of unnecessary scripts.
  * Keep your homepage lightweight.

***

### 9. Preview vs. Built App Differences

#### 9.1 Some features don’t work in the Preview App

* **Why it happens:** The Preview App does not support every native feature.
* **How to fix:**
  * Use the Preview App to check layout and navigation.
  * Do a full build to test native features like notifications, deep links, and in-app purchases.

***

### 10. General Tips

* Always use **HTTPS** for all assets and APIs.
* Use **stable URLs** for important screens.
* Mark **checkout and external services** as external links.
* Always add **clear permission descriptions**.
* Test your Base44 app on a real phone before wrapping it.
* Rebuild your app in BuildNatively after changing native features.

### 11. App Store & Play Store Rejections

#### 11.1 My app was rejected for being “just a website”

* **Why it happens:** Apple requires apps to offer **native value** beyond a simple web wrapper.
* **How to fix:**
  * Add at least one **native feature** in BuildNatively (push notifications, deep links, native sharing, camera, location, in-app purchases, etc.).
  * Resubmit after enabling these features.

#### 11.2 My app was rejected for payments not using in-app purchases

* **Why it happens:** Apple and Google require you to use their **In-App Purchases (IAP)** for **digital goods, content, or subscriptions**. Using Stripe or PayPal for digital items leads to rejection.
* **How to fix:**
  * If you sell **digital items** (ebooks, lessons, access to content, memberships): enable **In-App Purchases** (via RevenueCat) in BuildNatively.
  * If you sell **physical goods or services** (clothing, coaching, bookings): you can use Stripe/PayPal.
  * Update your app and resubmit.

#### 11.3 My app was rejected for missing permission descriptions

* **Why it happens:** Apple requires **clear reasons** for every permission (camera, microphone, contacts, location, etc.).
* **How to fix:**
  * In BuildNatively, add clear descriptions for each enabled permission.
  * Examples:
    * Camera: “We use your camera to let you upload a profile picture.”
    * Location: “We use your location to show nearby events.”
    * Microphone: “We use your microphone to let you record voice messages.”
  * Rebuild and resubmit.

#### 11.4 My app was rejected for “broken links” or “poor navigation”

* **Why it happens:** Internal links may not be set up correctly, or external links weren’t marked as external.
* **How to fix:**
  * Test all links inside your app.
  * Mark external links (YouTube, Stripe, Calendly, external docs) as **external** in BuildNatively so they open in the device browser.
  * Rebuild and resubmit.

#### 11.5 My app was rejected for “slow loading” or “poor performance”

* **Why it happens:** Your Base44 app loads too slowly on mobile devices.
* **How to fix:**
  * Optimize Base44 by reducing large images, videos, and heavy scripts.
  * Keep your homepage lightweight so the app starts faster.
  * Resubmit after improving performance.

#### 11.6 My app was rejected for “insufficient content”

* **Why it happens:** Stores sometimes reject apps with little or placeholder content.
* **How to fix:**
  * Make sure your Base44 app is complete and has real content (not “Coming Soon” pages).
  * Test your app end-to-end before resubmitting.

#### 11.7 My app was rejected for “not following guidelines”

* **Why it happens:** Each store has rules on content, payments, and privacy. Common issues include:
  * No privacy policy page.
  * Selling digital goods without IAP.
  * Asking for permissions without justification.
* **How to fix:**
  * Add a **Privacy Policy** page to your Base44 app and link it in the app store listings.
  * Review Apple and Google guidelines for your type of app.
  * Rebuild and resubmit with fixes applied.

***


# Replit

Good news! If you’ve built a web app with Replit, you can now turn it into a real mobile app for the App Store and Google Play using BuildNatively — no coding required.

Replit helps you create and host your web apps, while BuildNatively wraps your project into a mobile app and unlocks powerful native features like push notifications, deep links, in-app purchases, geolocation, and more.

This guide will walk you step by step through everything you need to do in **Replit** and in **BuildNatively**, so your app works perfectly on iOS and Android.

***

### What You’ll Need Before You Start

* A **Replit web app** (already created and running).
* A **secure HTTPS link** to your Replit app.
* A **BuildNatively account**.
* Optional but recommended:
  * An **Apple Developer account** ($99/year).
  * A **Google Play Developer account** ($25 one-time).

***

### Part 1 — Prepare Your Replit App

Before connecting to BuildNatively, make sure your Replit app is set up correctly.

#### 1) Publish with HTTPS

* Replit apps must run over **https\://** (Replit provides this automatically for public web apps).
* Make sure every resource (images, scripts, APIs) also uses HTTPS.
* Apps that load **http\://** resources will not work in iOS/Android.

***

#### 2) Use a Single Domain

* Keep all of your app’s pages on one domain (the Replit-provided domain or a custom domain you’ve set).
* Avoid switching between multiple subdomains for important flows like login or checkout.
* If you must use an external domain (for payments, external services), you’ll mark it as **external** in BuildNatively later.

***

#### 3) Create Stable URLs for All Important Pages

* Every key part of your app (login, signup, profile, checkout, dashboard, orders, messages) should have its own **clean, shareable URL**.
* Examples:
  * `/login`
  * `/signup`
  * `/profile`
  * `/orders/123`
* These stable URLs are essential for deep links, notifications, and navigation.

***

#### 4) Avoid Popups for Critical Actions

* Popups or modals for login, signup, or checkout don’t generate unique URLs.
* This breaks deep linking, push notifications, and return flows after login.
* Instead, create **dedicated pages** for these flows in your Replit app.

***

#### 5) Test in a Mobile Browser

* Open your Replit app on both **iPhone (Safari)** and **Android (Chrome)**.
* Check that:
  * Text and buttons fit the screen.
  * Forms work without zooming.
  * Pages scroll smoothly.
  * Images and videos load quickly.
* Fix any layout issues directly in Replit before moving forward.

***

#### 6) Plan Your Login System

* **Email/password login**: If it works in mobile browsers, it will work in the app.
* **Social login (Google, Apple, Facebook)**:
  * Requires **deep links (Universal Links/App Links)** so users are returned to your app after login.
  * You’ll configure this in BuildNatively later.
* Make sure your login flow redirects back to your Replit domain.

***

#### 7) List Your External Links

* Identify any external sites your app uses (Stripe, PayPal, YouTube, Calendly, Help Docs, etc.).
* You’ll mark these as **external** in BuildNatively so they open in the device’s browser instead of inside the app.

***

#### 8) Optimize Performance

* Compress images and videos to reduce load times.
* Avoid very heavy scripts that slow down startup.
* Keep your homepage lightweight so the mobile app loads fast.

***

### Part 2 — Create Your Mobile App in BuildNatively

1. Log in to your **BuildNatively dashboard**.
2. Click **Create New App**.
3. Enter your app’s name.
4. Paste your Replit app’s **HTTPS URL**.
5. Save — your Replit app is now connected to BuildNatively.

***

### Part 3 — Customize Your App

* **App Icon**: Upload a **1024×1024 PNG** (no transparent background).
* **Splash Screen**: Upload a **2048×2048 PNG** (shown while your app loads).
* **Error Screen**: Upload another **2048×2048 PNG** (shown if the network fails).
* **Navigation**: Choose between Bottom Bar, Top Bar, or no navigation. Configure tabs if using Bottom Bar.
* **Colors & Style**: Adjust background, theme, and accent colors.
* **Permissions**: Add clear, user-friendly descriptions for any native features you enable (e.g., “We use your camera to let you upload photos”).

***

### Part 4 — Configure Links

* Add your **Replit domain** as an **internal link** so it stays inside the app.
* Add any external services (Stripe, PayPal, Calendly, YouTube, external docs) as **external links** so they open in the device’s browser.
* This prevents errors or users getting “stuck.”

***

### Part 5 — Enable Native Features

You can enable these anytime. Rebuild your app if you make changes.

* **Push Notifications**: Use OneSignal or Firebase. Include deeplinks so users land on the right screen when tapping a notification.
* **Deep Links**: Allow users to open specific screens (e.g., `/orders/123`) directly from links or notifications. Required for social login.
* **Social Login**: Enable Google, Apple, or Facebook login by turning on **Social Auth** + **Deep Links**.
* **In-App Purchases**: Use RevenueCat for digital subscriptions or content.
* **Other Features**: Enable geolocation, camera, QR scanning, contacts, analytics, and more as needed.

***

### Part 6 — Preview Your App

* Install the **BuildNatively Preview App** on your phone.
* Log in and preview your Replit app inside it.
* Use this to check layout, navigation, and styling.
* Remember: push, deep links, social login, in-app purchases, and some native features **only work after a full build**.

***

### Part 7 — Build & Publish

1. Build for **iOS and Android** inside your BuildNatively dashboard.
2. For iOS:
   * Connect your Apple Developer account.
   * Test in TestFlight.
   * Submit to the App Store.
3. For Android:
   * Connect your Google Play Developer account.
   * Upload the APK/AAB to Google Play Console.
   * Publish to the Play Store.

***

### Part 8 — After Publishing

* Update your Replit app anytime — changes appear instantly in your mobile app (no rebuild needed).
* If you change **native features** (push, payments, deep links, navigation), rebuild in BuildNatively.
* Use push notifications and deep links to engage users.
* Track analytics if enabled.

***

### Troubleshooting: Common Issues

#### Login & Auth

* **Problem:** Login opens in browser and doesn’t return.
* **Fix:** Enable Deep Links + Social Auth, rebuild app.
* **Problem:** Blank screen after login.
* **Fix:** Make sure login redirects to a full page with a URL, not a popup.

***

#### Payments

* **Problem:** Stripe/PayPal checkout doesn’t work.
* **Fix:** Mark checkout pages as external in BuildNatively.
* **Problem:** Apple rejected app for payment handling.
* **Fix:** Use In-App Purchases for digital goods/subscriptions.

***

#### Notifications & Deep Links

* **Problem:** Notification arrives but does nothing.
* **Fix:** Add a unique deeplink to the notification payload (e.g., `/messages/123`).
* **Problem:** Notifications don’t arrive.
* **Fix:** Check push config, rebuild app, and test again.

***

#### Layout & Display

* **Problem:** Pages cut off on small screens.
* **Fix:** Adjust layout in Replit for mobile-first design.
* **Problem:** iOS zooms in on forms.
* **Fix:** Use at least 16px font size on input fields.

***

#### App Store & Play Store Rejections

* **Problem:** “Just a website” rejection.
* **Fix:** Add native features like push notifications, deep links, or camera.
* **Problem:** Missing permission descriptions.
* **Fix:** Add clear permission texts in BuildNatively before rebuilding.
* **Problem:** Performance issues.
* **Fix:** Optimize images/videos in Replit and reduce heavy scripts.

***

### Quick Start Checklist

1. Replit app is live with HTTPS.
2. Each important screen has a unique URL (no popups for login/checkout).
3. Layout tested on real phones for mobile friendliness.
4. External links (Stripe, YouTube, Calendly) listed and configured as external in BuildNatively.
5. App icon (1024×1024 PNG) and splash/error screens (2048×2048 PNG) ready.
6. Optional: Push notifications, deep links, and social login configured.
7. Preview app tested, then full builds created.
8. Submitted to App Store and Google Play.

***

### Why This Works Well for Replit Users

Replit gives you a fast way to build and deploy web apps. BuildNatively takes those apps and makes them **installable mobile apps** with powerful native features and app store distribution. Together, they let you go from idea to a live mobile app — without needing advanced coding or app development tools.


# Quick Start Checklist

Follow this step-by-step checklist to turn your Replit app into a fully functional iOS and Android app with BuildNatively.

### ✅ Step 1: Prepare Your Replit App

* Your Replit app is **live with an HTTPS link** (Replit provides this automatically for public projects).
* All assets (images, scripts, APIs) load over **HTTPS** — no `http://` content.
* Your app uses **one primary domain** (the default Replit domain or a custom one).
* Each important screen has a **unique, shareable URL**:
  * `/login` for login
  * `/signup` for signup
  * `/profile` for user profiles
  * `/checkout` for payments
* Critical flows like login, signup, and checkout are on **full pages** (not only popups or modals).
* Test your Replit app in a **mobile browser** (Safari on iPhone, Chrome on Android):
  * Buttons are tap-friendly.
  * Text fits without zooming.
  * Forms scroll smoothly.
  * Images and videos load quickly.
* Make a list of **external links** used in your app (Stripe, PayPal, Calendly, YouTube, Help Docs). You’ll configure these in BuildNatively later.

***

### ✅ Step 2: Create Your Mobile App in BuildNatively

* Log in to your BuildNatively dashboard.
* Click **Create New App**.
* Enter your app name.
* Paste your Replit app’s **HTTPS URL**.
* Save your new app.

***

### ✅ Step 3: Customize Appearance

* **App Icon**: Prepare a **1024×1024 PNG** (no transparent background).
* **Splash Screen**: Prepare a **2048×2048 PNG** (shown when the app loads).
* **Error Screen**: Prepare another **2048×2048 PNG** (shown if the network fails).
* **Navigation Bar**: Decide if you want a **Bottom Bar, Top Bar, or none**. Add tab URLs if using a Bottom Bar.
* **Colors & Style**: Adjust background and accent colors to match your brand.

***

### ✅ Step 4: Configure Links

* Add your **Replit domain** as **internal** so pages open inside the app.
* Add third-party sites (Stripe, YouTube, Calendly, PayPal, Help Docs) as **external** so they open in the device’s browser.
* Test each link type:
  * Internal → should stay inside the app.
  * External → should open outside in Safari/Chrome.

***

### ✅ Step 5: Enable Native Features (Optional but Recommended)

* **Push Notifications**: Choose OneSignal or Firebase. Configure deeplinks so users land on the right page when tapping a notification.
* **Deep Links (Universal Links/App Links)**: Needed for Social Login and helpful for notifications.
* **Social Login (Google, Apple, Facebook)**: Enable Social Auth + Deep Links in BuildNatively.
* **In-App Purchases**: Enable via RevenueCat if selling digital goods or subscriptions.
* **Other Features**: Turn on extras like geolocation, camera, QR scanning, contacts, analytics, or haptics depending on your app.

***

### ✅ Step 6: Test Your App

* Install the **BuildNatively Preview App** on your phone.
* Log in and open your Replit app inside it.
* Test layout, navigation, and content flow.
* Remember: Some features (push, deep links, social login, in-app purchases) will **not work in Preview** — they require a full build.

***

### ✅ Step 7: Build & Publish

* In your BuildNatively dashboard, generate **iOS and Android builds**.
* **For iOS:**
  * Connect your Apple Developer account.
  * Test the build in **TestFlight**.
  * Submit to the **App Store**.
* **For Android:**
  * Connect your Google Play Developer account.
  * Upload the APK/AAB to **Google Play Console**.
  * Publish to the **Play Store**.

***

### ✅ Step 8: After Publishing

* Update your Replit app anytime — changes appear instantly in your mobile app (no rebuild needed).
* If you change **native settings** (push notifications, navigation style, deep links, in-app purchases), rebuild the app in BuildNatively.
* Use push notifications + deeplinks to engage your users.
* Track analytics if you’ve enabled them.
* Monitor app reviews in App Store and Play Store and update regularly.

***

### 🚀 Pro Tips for Success

* Keep your app lightweight → compress large images and videos.
* Always mark **checkout/payment links as external** to avoid errors.
* Use **stable page URLs** for login, profile, checkout, and notifications.
* Write clear **permission descriptions** (e.g., “We use your location to find nearby events”).
* Test everything on real devices before submitting to stores.


# Integration

To ensure your Base44 app works smoothly once converted into a native mobile app, you’ll need to prepare your Base44 project in specific ways. These steps make sure navigation, login, payments, and notifications all work correctly inside iOS and Android apps.

***

#### 1) Publish Your Base44 App with HTTPS

* Your Base44 app must be live on a **secure HTTPS link**.
* iOS and Android block **http\://** content for security reasons.
* If any image, API, or script inside your Base44 app still uses `http://`, update it to `https://`.
* Test by opening your app in Chrome or Safari → if you see a “Not Secure” warning, fix before moving forward.

***

#### 2) Use a Single Primary Domain

* Keep your app hosted on **one main domain** (for example, `app.example.com`).
* Avoid sending users to different subdomains like `login.example.com` or `checkout.example.com`.
* If certain flows must use a different domain (like Stripe or PayPal checkout), you’ll need to mark these links as **external** in BuildNatively.
* Staying on one domain makes login, deep links, and navigation seamless.

***

#### 3) Give Every Important Screen Its Own URL

* Make sure all critical pages in your Base44 app can be reached via a **unique, stable URL**.
* Examples:
  * `/login` → Login screen
  * `/signup` → Signup screen
  * `/profile` → Profile page
  * `/orders/123` → Order details
  * `/checkout` → Checkout page
* Why this matters:
  * Push notifications can deeplink users directly to a page.
  * Social logins return users to the right screen after authentication.
  * Users can share or revisit pages reliably.

***

#### 4) Avoid Popups for Critical Flows

* Login, signup, onboarding, and checkout should not exist **only** as popups or modals.
* Popups do not change the URL, which breaks:
  * Deep linking
  * Social login redirects
  * Push notification routing
* Instead, design these flows as **dedicated full pages** in Base44.

***

#### 5) Test Your App on Real Mobile Devices

* Open your Base44 app in Safari (iPhone) and Chrome (Android).
* Test every page and flow:
  * Do text and buttons fit the screen?
  * Are forms easy to complete without zooming?
  * Does scrolling feel smooth?
  * Do images and videos load quickly?
* Fix any layout problems in Base44 before integrating with BuildNatively — the mobile app will display your web content exactly as it appears in a phone browser.

***

#### 6) Plan for Login & Authentication

* **Email/Password Login**: If it works in Safari/Chrome, it will also work inside your app.
* **Google, Apple, or Facebook Login**:
  * These require **deep links** (Universal Links/App Links).
  * Make sure your Base44 login flow redirects users back to your app’s primary domain after login.
  * Later, enable **Social Auth + Deep Links** in BuildNatively to complete the setup.

***

#### 7) Identify All External Links

* Make a list of every link in your Base44 app that goes outside your domain.
* Common examples:
  * Stripe or PayPal checkout
  * Calendly scheduling pages
  * YouTube or Vimeo video embeds
  * External Help Docs
* In BuildNatively, configure these as **external links** so they open in Safari/Chrome instead of inside your app.
* This prevents broken experiences or being “stuck” in the wrong view.

***

#### 8) Prepare Checkout & Payments

* If you’re using an external checkout provider (like Stripe), test your payment flow on a real mobile device.
* If your checkout redirects to an external page, mark it as **external** in BuildNatively.
* If you sell **digital goods or subscriptions** (courses, content, memberships), Apple and Google require you to use **In-App Purchases** instead of external payments.
* If you sell **physical products or services**, Stripe/PayPal is fine.

***

#### 9) Optimize Performance for Mobile

* Compress large images (JPG/PNG) and videos before uploading.
* Use lazy-loading where possible for heavy content.
* Simplify your homepage design — the faster your app loads, the better the user experience.

***

#### 10) Define Routes for Notifications & Deep Links

* Push notifications and deep links depend on stable URLs.
* Example flows:
  * A “New Message” notification should open `/messages/123`.
  * An “Order Shipped” notification should open `/orders/456`.
* Make sure these routes exist in your Base44 project and open the correct content when tested in a browser.

***

#### ✅ Summary of Integration Requirements

Before wrapping your Base44 app with BuildNatively, confirm that:

1. Your Base44 app runs on **HTTPS** (no insecure content).
2. You’re using **one main domain** for internal navigation.
3. All key screens (login, signup, profile, checkout, orders) have **unique URLs**.
4. Login/signup/checkout are **full pages**, not only popups.
5. You’ve tested your app on real phones for **mobile readiness**.
6. Your login flow (especially with Google/Apple/Facebook) redirects back to your **primary domain**.
7. You have a list of **external links** (payments, scheduling, videos, docs) ready to configure.
8. Your checkout/payment flow works correctly and follows **App Store rules**.
9. Large media is optimized for **fast mobile performance**.
10. All notification and deep link routes exist as **real pages with URLs**.


# Troubleshooting Guide

Here’s a detailed list of the most common issues you may encounter when turning your Replit project into a mobile app with BuildNatively, along with explanations and fixes.

### 1. Login & Authentication Issues

#### 1.1 My login opens in Safari/Chrome instead of inside the app

* **Why it happens:** Social login (Google, Apple, Facebook) requires deep links (Universal Links/App Links) to bring users back to the app.
* **How to fix:**
  1. Enable **Deep Links** in BuildNatively.
  2. Turn on **Social Auth** in the Features section.
  3. Rebuild your app.

***

#### 1.2 My email/password login fails in the app but works in Replit

* **Why it happens:** Cookies or session handling may break if redirects jump across domains.
* **How to fix:**
  * Keep login pages on your main Replit domain (or custom domain).
  * Avoid sending users to another subdomain for authentication.
  * Make sure your entire app runs on **HTTPS**.

***

#### 1.3 I get stuck after login (blank page or endless spinner)

* **Why it happens:** Post-login redirects may point to a popup instead of a full page.
* **How to fix:**
  * In Replit, create a dedicated page (like `/dashboard`) for the post-login redirect.
  * Update your login flow to redirect there.

***

### 2. Payments & Checkout Issues

#### 2.1 Stripe/PayPal checkout doesn’t work in the app

* **Why it happens:** Payment providers often require the phone’s browser to handle checkout securely.
* **How to fix:**
  * Mark checkout pages as **external** in BuildNatively.
  * This ensures they open in Safari/Chrome and complete successfully.

***

#### 2.2 Apple/Google rejected my app because of payments

* **Why it happens:** Stores require In-App Purchases for **digital goods and subscriptions**.
* **How to fix:**
  * For digital content: enable **In-App Purchases** (RevenueCat) in BuildNatively.
  * For physical goods or real-world services: you may keep Stripe/PayPal.

***

### 3. Push Notifications & Deep Links

#### 3.1 Notifications arrive but nothing happens when tapped

* **Why it happens:** The notification didn’t include a deep link.
* **How to fix:**
  * Add a URL in the notification payload (e.g., `/messages/123`).
  * Make sure that URL exists and works in your Replit app.

***

#### 3.2 Notifications open the wrong page

* **Why it happens:** The link in the notification doesn’t match your Replit routes.
* **How to fix:**
  * Double-check that the link exactly matches your app’s route structure.

***

#### 3.3 Notifications don’t show up at all

* **Why it happens:** Push service setup is incomplete.
* **How to fix:**
  * Confirm push is enabled in BuildNatively.
  * Upload correct configuration (OneSignal/Firebase).
  * Rebuild your app and test again.

***

### 4. Layout & Display Issues

#### 4.1 Some pages look broken or cut off on small screens

* **Why it happens:** The Replit layout isn’t mobile-optimized.
* **How to fix:**
  * Adjust your CSS in Replit for responsive design.
  * Stack elements vertically instead of side-by-side.

***

#### 4.2 iOS zooms in too much on forms

* **Why it happens:** iOS zooms when input fields have small font sizes.
* **How to fix:**
  * In Replit, set input font sizes to **at least 16px**.

***

#### 4.3 Scrolling feels broken

* **Why it happens:** Nested scrollable areas or large fixed sections can conflict with native scrolling.
* **How to fix:**
  * Simplify layouts and avoid multiple nested scroll containers.

***

### 5. Links & Navigation

#### 5.1 External links don’t work (YouTube, Calendly, Docs)

* **Why it happens:** By default, BuildNatively treats all links as internal.
* **How to fix:**
  * Add these domains as **external links** in BuildNatively so they open in the device’s browser.

***

#### 5.2 Internal links reload the app instead of navigating

* **Why it happens:** The link points to a different domain or subdomain.
* **How to fix:**
  * Keep all internal navigation on your main Replit domain.

***

#### 5.3 Bottom bar tabs don’t load correctly

* **Why it happens:** Each tab must point to a valid page inside your app.
* **How to fix:**
  * Double-check that tab URLs match real Replit routes.

***

### 6. Media, Camera & Files

#### 6.1 Camera or microphone don’t work

* **Why it happens:** Permissions weren’t set.
* **How to fix:**
  * Enable **Camera** or **Microphone** in BuildNatively.
  * Add clear permission descriptions.
  * Rebuild the app.

***

#### 6.2 File uploads fail

* **Why it happens:** Upload endpoints may be insecure or unstable.
* **How to fix:**
  * Ensure all upload routes use **HTTPS**.
  * Test uploads on full pages instead of popups.

***

#### 6.3 Images and videos load too slowly

* **Why it happens:** Media files are too large.
* **How to fix:**
  * Compress images/videos before uploading to Replit.

***

### 7. Location & Permissions

#### 7.1 Location services don’t work

* **Why it happens:** The feature wasn’t enabled.
* **How to fix:**
  * Enable **Geolocation** in BuildNatively.
  * Add a clear permission description.
  * Rebuild the app.

***

#### 7.2 Apple rejected my app for missing permission text

* **Why it happens:** Apple requires a description for each permission.
* **How to fix:**
  * Add clear explanations in BuildNatively.
  * Examples:
    * “We use your location to find nearby events.”
    * “We use your contacts to help you invite friends.”

***

### 8. Publishing Issues

#### 8.1 Apple rejected my app as “just a website”

* **Why it happens:** Apple expects apps to offer **native features**.
* **How to fix:**
  * Add push notifications, deep links, or other native features.
  * Rebuild and resubmit.

***

#### 8.2 My icon or splash screen didn’t update

* **Why it happens:** Old assets were cached.
* **How to fix:**
  * Upload new assets in BuildNatively.
  * Rebuild your app.
  * Uninstall old versions from your phone before testing.

***

#### 8.3 App loads slowly on startup

* **Why it happens:** The homepage in Replit is too heavy.
* **How to fix:**
  * Optimize large images/videos.
  * Remove unnecessary scripts.
  * Keep the homepage simple.

***

### 9. Preview vs. Full Build

#### 9.1 Features don’t work in the Preview App

* **Why it happens:** Some native features aren’t supported in Preview.
* **How to fix:**
  * Use Preview for layout and navigation checks.
  * Do a full build to test push, deep links, social login, and purchases.

***

### 10. General Tips

* Always use **HTTPS** everywhere.
* Create **stable routes** for login, profile, checkout, and dashboard.
* Mark checkout and third-party services as **external**.
* Add clear **permission descriptions** before submitting to app stores.
* Optimize Replit content for mobile.
* Rebuild your app whenever you change **native settings**.

***

### 11. App Store & Play Store Rejections

#### 11.1 “Just a website” rejection

* **Fix:** Add native features (push, deep links, sharing, camera, location).

#### 11.2 Payment policy rejection

* **Fix:** Use In-App Purchases for digital goods/subscriptions. Stripe/PayPal only for physical goods.

#### 11.3 Missing permission descriptions

* **Fix:** Add clear, user-friendly explanations in BuildNatively.

#### 11.4 Broken or poor navigation

* **Fix:** Test all links. Mark external ones correctly.

#### 11.5 Performance issues

* **Fix:** Optimize Replit homepage and assets.

#### 11.6 Insufficient content

* **Fix:** Fill your app with real, working content before submission.

#### 11.7 Privacy or policy rejection

* **Fix:** Add a **Privacy Policy** page in your Replit app and link it in store listings.

***

### Quick Fix Checklist

* ✅ HTTPS everywhere.
* ✅ One domain for internal pages.
* ✅ Stable URLs for login, profile, checkout.
* ✅ External links configured correctly.
* ✅ Permission descriptions added.
* ✅ Push, deep links, and purchases set up if needed.
* ✅ Layout tested on real phones.
* ✅ Rebuilt after any native changes.


# Subscription Plans

Natively offers four plans to fit different stages of app development. You can start for free and upgrade at any time.

[See full pricing](https://www.buildnatively.com/pricing)

## Natively plans <a href="#natively-plans" id="natively-plans"></a>

**Free** - Explore Natively and test your app on a real device using the [Preview](/natively-platform/preview) mode before committing to a paid plan.

**Essential** - A basic set of native features with the ability to publish your app for both iOS and Android. Includes a limited number of rebuilds. Good for simple apps that don't require advanced native functionality.

**Unlimited -** Full set of native features, unlimited rebuilds, and the ability to publish your app for both iOS and Android. Recommended for production apps.

**Lifetime** - Same as in Unlimited, with a one-time payment - no recurring fees.

## **Feature comparison**

<table data-header-hidden="false" data-header-sticky><thead><tr><th align="center">Features</th><th align="center">Free</th><th align="center">Essential</th><th align="center">Unlimited/Lifetime</th></tr></thead><tbody><tr><td align="center"><a data-footnote-ref href="#user-content-fn-1">Appearance</a></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td></tr><tr><td align="center"><a data-footnote-ref href="#user-content-fn-2">Basic Features</a></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td></tr><tr><td align="center"><a data-footnote-ref href="#user-content-fn-3">Advanced Features</a></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="274c">❌</span></td><td align="center"><span data-gb-custom-inline data-tag="emoji" data-code="2705">✅</span></td></tr></tbody></table>

[^1]: Appearance include App Icon, Loading Screen, Launch Screen(iOS), Error Screen and Bottom Bar.

[^2]: Basic Features are Camera, Microphone, Contacts, Calendars, and others.

[^3]: Advanced features like Push Notification, In-App Purchases, Deeplinks, Social Auth, Geolocation, NFC, and others.


# Releases

To access these updates, ensure your app build date and Bubble Plugin/JavaScript SDK versions match the requirements below.

## April 17, 2026

#### v2.29.0 Bubble Plugin | v2.26.0 JavaScript SDK

✨ New Features

* [Localization](https://docs.buildnatively.com/guides/integration/localization) – Expand your app’s global reach with native multi-language support. This includes AI-powered auto-translation for system permission strings (ATT, Camera, etc.) to ensure your app remains compliant across all App Store regions.
* [Native Audio Player](https://docs.buildnatively.com/guides/integration/audio-player) – A high-performance audio player designed for seamless playback. It supports background audio, streaming, and advanced metadata handling, making it perfect for podcasts, music, and media-heavy applications.

## March 11, 2026

### v2.28.0 Bubble Plugin | v2.25.2 JavaScript SDK

#### ✨ New Features

* [Apple ATT](/guides/integration/apple-att) - Gain control over iOS tracking permissions. You can now manually trigger the consent popup or check the authorization status to ensure your app is compliant with Apple’s privacy guidelines.

#### 🚀 Improvements

* iOS Tap Delay Optimization – We’ve the issue with delay for touch interactions on iOS.

## January 30, 2026

### v2.27.0 Bubble Plugin | v2.22.0 JavaScript SDK

#### 🚀 Improvements

* [In-app purchases](/hidden-pages/in-app-purchases) - We have added support for Google Play's five proration modes. This allows you to customize how subscription changes (upgrades/downgrades) are handled - whether the change happens immediately with a price adjustment or at the start of the next billing cycle.

## December 18, 2025

### v2.26.0 Bubble Plugin | v2.20.0 JavaScript SDK

#### ✨ New Features

* [Custom Error Handler](/guides/integration/device-info) - You can now intercept errors to trigger your own custom workflows and UI logic, replacing default Error screen.
* [Service Worker Support](/natively-platform/features/service-worker) - Enable resource caching (including specific hosts) to significantly improve app loading speed and support offline capabilities.

#### 🚀 Improvements

* Dashboard: The app's plan is now displayed for each app individually on the Apps screen.
* Settings: Added a one-click Copy button next to the App ID in Settings for faster access.

## November 13, 2025

### v2.25.0 Bubble Plugin | v2.19.0 JavaScript SDK

#### ✨ New features

* [Send Custom Event](/natively-platform/features/analytics/custom-event) - This feature allows you to send custom events and associated metadata directly to your configured Analytics service provider (e.g., AppsFlyer, Facebook)
* [PDF Viewer](/guides/integration/pdf-viewer) - View PDF files instantly within the app environment, eliminating the need to download the document first or redirect the user to an external device browser

#### 🚀 Improvements

* Subscription Inactive Banner: Added a Refresh button to re-validate the app's plan status.

## v 2.12.0

* Custom [Loading](/natively-platform/appearance/launch-screen) and [Error](/natively-platform/appearance/network-screen) screens \[Beta]
* [Custom Notification Sound](/natively-platform/features/notifications/onesignal-push-notifications#bubble-custom-notification-sound) \[Beta]
* [Ability to request push permission on app launch (No SDK needed!)](/hidden-pages/onesignal-notifications)&#x20;
* [Control Custom Loading screen visibility via SDK ](/guides/integration/loading-screen)
* [Android Status Bar customization](/natively-platform/appearance/style#status-bar-style)
* Small improvements and bug fixes&#x20;
* A fresh look at the Pricing page&#x20;
* Introducing graduated pricing for Lifetime for Agencies & Freelancers: The more you buy, the more you save (up to 50% off)!
* Apple Sign In Beta -> Release
* NFC Beta -> Release
* Admob Beta -> Release

***


# Preview

Test your app before committing to a paid plan - either instantly in your browser or on a real device.

## What is Preview?

Preview lets you see how your app will look and behave before buying a build. Natively offers two ways to preview:

**Natively Dashboard Preview** - the fastest option. Renders your app in a device frame directly in the Natively Dashboard, letting you check layout and navigation instantly, without installing anything.

**Natively Preview App** - install the Natively Preview app on a real device to test your app more thoroughly, including a wider (but still limited) set of native features. Use this when your website can't load properly in the browser preview, or when you want to check things on an actual device.

## **Natively Dashboard** Preview

1. Open your Natively app dashboard and go to **Preview** in the left sidebar.
2. Wait for your app to load in the device frame.
3. Use the icons on the side to switch between **iOS/Android**, **phone/tablet**, or **portrait/landscape** views.

{% hint style="danger" %}
**Natively Dashboard Preview** only supports a small set of purely visual native features - such as [Bottom Bar](/natively-platform/features/bottom-bar) and [Style](/natively-platform/appearance/style) settings. Most native features, including anything requiring device hardware, permissions, or a real build, will not work here.
{% endhint %}

<figure><img src="/files/LGYy8EPzzdprBGCcvjmo" alt="" width="187"><figcaption><p>Browser Preview example</p></figcaption></figure>

## Natively Preview App

1. Install the Natively Preview app from the [App Store](https://apps.apple.com/us/app/natively-preview/id1626045691) or [Google Play](https://play.google.com/store/apps/details?id=com.ncnp.app).
2. Open your app's dashboard and go to **Preview** in the left sidebar.
3. Scan the QR code with your phone's camera, or open the Natively Preview app directly, log in to your Natively account, and select your app.

{% hint style="danger" %}
Appearance settings you've configured (App Icon, Loading Screen, Launch Screen, Error Screen, Style) are **not reflected** in the Preview App.
{% endhint %}

<div><figure><img src="/files/2TYAuxpVWVGH4mVruK68" alt="" width="188"><figcaption><p>Natively Preview app example</p></figcaption></figure> <figure><img src="/files/DFkCjWACpJVmOqSCxlMe" alt="" width="188"><figcaption><p>App opened in Natively Preview</p></figcaption></figure></div>

## Known limitations

{% hint style="danger" %}
Both preview methods run in a sandboxed environment. Any feature relying on native store accounts, real device signing, or server-side callbacks tied to your production app is unlikely to work correctly - even if it doesn't throw an obvious error. This applies more to Natively Dashboard Preview than the Natively Preview App, which supports a broader (though still limited) set of native features.
{% endhint %}

{% hint style="danger" %}
Features confirmed **not** to work in the Natively Preview App:

* **In-App Purchases**
* **Deep Links**
* **Social Auth**
* **Background Geolocation**
* **Analytics**
* **HealthKit**
* **AdMob**
* **Calendars**
* **Camera**
* **Audio Recorder**
* **Audio Player**
  {% endhint %}

{% hint style="warning" %}
This list reflects known limitations, not a complete guarantee. If something doesn't work as expected in either preview method, test with a full build before assuming it's broken.
{% endhint %}


# Appearance

The Appearance section is where you customize how your app looks and feels - your app icon, loading screen, error screen, in-app styling, and bottom navigation bar.

{% hint style="warning" %}
Any changes made in the Natively Dashboard, including Appearance settings, require a rebuild to take effect.
{% endhint %}

{% content-ref url="/pages/4HbWFmsWKMPy8Ftz1QTp" %}
[App Icon](/natively-platform/appearance/app-icon)
{% endcontent-ref %}

{% content-ref url="/pages/dU5GdyHVYQBoZpOQNrM0" %}
[Loading Screen](/natively-platform/appearance/launch-screen)
{% endcontent-ref %}

{% content-ref url="/pages/2fMewGN4zvP0HoqDJkmq" %}
[Launch Screen (iOS)](/natively-platform/appearance/launch-screen-ios)
{% endcontent-ref %}

{% content-ref url="/pages/ZapS5BKuHXr5t5U5FSvy" %}
[Error Screen](/natively-platform/appearance/network-screen)
{% endcontent-ref %}

{% content-ref url="/pages/VT31AYsRlSRshKmxpwss" %}
[Style](/natively-platform/appearance/style)
{% endcontent-ref %}


# App Icon

Your app icon the first thing users will see when they install your application.

{% hint style="warning" %}
An app icon is strictly required to generate your app build. Without configuring an app icon, you will not be able to create a final build.
{% endhint %}

### Configure Your App Icon

Natively offers two automated ways to set up your app icon using our integrated AI generation tools. Navigate to **Appearance > App Icon** in your dashboard to get started.

#### Option 1: Generate from a Text Description

If you don't have a pre-made asset, you can use AI to generate a custom icon directly from a text prompt as shown in the screenshot.

1. Select the **Describe with AI** tab.
2. In the **Describe your icon** text field, type a detailed prompt.
   * *Tip: Be specific about the subject, style, and colors (e.g., "A minimalist purple rocket on a deep navy gradient, glossy and modern").*
3. Alternatively, click one of the quick suggestions below the input box (such as *"Minimal fitness app"* or *"Abstract geometric logo"*).
4. Click the **Generate icon** button.

In few seconds you will see the result.

<figure><img src="/files/dJ4yqzk5bYYoznkD2dr9" alt=""><figcaption></figcaption></figure>

#### Option 2: Generate from an Uploaded Image

If you have a reference image, logo, or rough sketch, you can use it as a base for the AI generation tool.

1. Select the **Upload image** tab next to the text prompt option.
2. Upload your source file.\
   **File Requirement:** The uploaded image must be in PNG or JPG format.
3. The tool will process your asset into a standardized mobile app icon layout.

<figure><img src="/files/UU2nGok4a1xITedvFaqI" alt=""><figcaption></figcaption></figure>

### Previewing and Managing Results

Once an icon is processed, it will automatically populate your layout and display a realistic mockup on the phone preview on the right side of the screen, as seen in the screenshot.

<figure><img src="/files/7NRrUJF5LXnAmL6a0F07" alt=""><figcaption></figcaption></figure>

* **Not happy with the result?** If the generated icon doesn't look quite right, you can easily delete it by clicking the red **Trash Can** icon. Once deleted, you can tweak your prompt or upload a different image and try again.
* When you are satisfied with the icon preview, click **Next** to save your changes and continue setting up your app's appearance.


# Loading Screen

The Loading Screen is a temporary transition view displayed to users while your App URL is loading in the background.

### OS Sequence Behavior

* **Android:** When the app is launched, users will see the App Icon first, followed immediately by the Loading Screen.
* **iOS:** When the app is launched, users will see the Launch Screen first, followed by the Loading Screen.

### Loading Screen Elements

Your loading screen is completely modular and can be built using any combination of the following 4 key elements:

1. **Background Color:** Sets the base color fill for the entire screen.
2. **Text Field:** Displays custom text on top of the background. You can fully customize the string, font type, color, size, and vertical/horizontal alignment coordinates.
3. **Background Image:** Applies an image behind your text and foreground assets.
4. **Image (Foreground):** Places a foreground graphic or loader animation on top of your background.

#### Popular Combinations

Because these options use simple toggle switches, you can combine them however you like:

* Background Color only (Minimalist approach).
* Background Color + Text Field (Simple branding).
* Background Image only (Full-screen illustration).

**Natively Preview App Style:** For a dynamic experience, combine a Background Color, a Background Image, and a Foreground Image.&#x20;

{% hint style="info" %}
The Foreground Image tool supports `.gif` files, allowing you to use a custom animated spinning loader!
{% endhint %}

### Configuring the Background Image with AI

When you turn on the Background image toggle, you can leverage Natively's automated asset tools instead of manually resizing templates.

#### Method A: Describe with AI

As shown in the screenshot, you can dictate exactly what your background should look like:

1. Select the **Describe with AI** tab.
2. Enter your creative description (e.g., *"A fantasy elven warrior"*).
3. Click **Generate background**.

<figure><img src="/files/vyZmIOqN7yDQwA1uTUjq" alt=""><figcaption></figcaption></figure>

#### Method B: Custom Image Upload

If you already have a design asset you want to use:

1. Select the **Upload image** tab.
2. Drag and drop your image file or click to browse.\
   **File Format Requirement:** Your uploaded reference file must be in PNG or JPG format.
3. Natively will automatically process your asset into a standardized mobile background layout.

<figure><img src="/files/Vt2ASj7IenjfXXkeu31x" alt=""><figcaption></figcaption></figure>

### Previewing and Managing Assets

Once an image is generated or uploaded, the layout syncs up with the interactive live preview on the right side of your dashboard screen.

* **Reviewing your design:** Check the mock device frame to verify that your layout components do not clash and that text elements remain readable.
* **Regenerating:** If a generated background or uploaded graphic is not sitting right in the phone frame, simply click the red **Trash Can** icon. This removes the asset, allowing you to test a new prompt or upload an adjusted file.
* **Saving:** When everything looks perfect, click Next/Save to lock in your settings.

<figure><img src="/files/DGSHjKclrTQihosbGYpv" alt=""><figcaption></figcaption></figure>


# Launch Screen (iOS)

The Launch Screen is a native splash view that appears on iOS devices immediately after a user opens your application, bridging the brief transition before the Loading Screen appears.

### Configure Your Launch Screen

Setting up your iOS Launch Screen follows the exact same AI-powered workflow as configuring your App Icon. Navigate to **Appearance > Launch Screen** in your dashboard to get started.

#### Option 1: Generate from a Text Description

You can leverage Natively’s integrated AI generator to create a custom splash asset directly from a prompt, as demonstrated in the screenshot.

1. Select the **Describe with AI** tab.
2. In the text field, enter a description of what your splash screen should look like (e.g., *"A rocket is waiting for start"*).
3. Alternatively, pick one of the optimized design suggestions located below the text input field (such as *"Minimal fitness app launch"* or *"Abstract geometric logo splash"*).
4. Click the **Generate iOS launch screen** button.

<figure><img src="/files/gQGxOt0h5QblHg8hmLvP" alt=""><figcaption></figcaption></figure>

#### Option 2: Generate from an Uploaded Image

If you have a primary layout, company logo, or brand asset ready, you can upload it to let the AI process it into a native splash screen frame, as shown in the screenshot.

1. Select the **Upload image** tab.
2. Drag and drop your graphic into the upload box or click to browse files.\
   **File Requirement:** Any asset uploaded here must be in PNG or JPG format.
3. Natively will process the asset to scale across all target Apple device resolutions automatically.

<figure><img src="/files/6AubNlPJDv3PmkjMsUvx" alt=""><figcaption></figcaption></figure>

### Previewing and Managing Results

Once the asset has finished processing, it will be added to your configuration and populate the realistic mobile mockup on the right side of the portal, as illustrated in the screenshot.

<figure><img src="/files/B5szbnwiYJhZiBpQeeEF" alt=""><figcaption></figcaption></figure>

* **Evaluating the Layout:** Use the live device simulation on the dashboard to ensure the proportions, text positioning, and color accents meet your standards.
* **Modifying Your Selection:** If you are unsatisfied with the generated graphic, you can remove it at any time. Simply click the red **Trash Can** icon to clear the frame and restart your generation process with a fresh prompt or reference image.
* **Saving Progress:** Click the Next button once the design is set to lock in your settings and advance to the next step.


# Error Screen

The Error Screen acts as a native safety fallback layout within your app.

### When Does the Error Screen Appear?

The Error Screen is displayed to users automatically whenever an underlying HTTP request fails completely. Common scenarios include:

* **Poor Network Conditions:** Extreme latency or intermittent internet drops.
* **Lost Signal:** Driving through tunnels or passing through dead zones.
* **Broken or Unsecured Links:** Accidental routing to insecure legacy paths (e.g., trying to access an `http://` address instead of a secure `https://` endpoint).

### Error Screen Layout Elements

You can customize the layout of your error fallback interface by toggling and configuring the following structural parameters within your platform dashboard:

1. **Background Color:** Define the base color fill for the entire screen background canvas.
2. **Text Field:** Display descriptive support guidelines directly over your color or asset layers. You can configure custom copy strings, font styling, scaling metrics, and coordinate alignments.
3. **Background Image:** Apply a full-screen contextual image or graphic overlay behind your structural elements.
4. **Image (Foreground):** Layer standalone graphical indicators, warning icons, or loaders.

### Configuring Background Graphics with AI

When the Background image switch is toggled active, you can generate or process your background image.

#### Method A: Text Descriptions via AI

As demonstrated in the screenshot, you can feed visual instructions straight to our generator engine:

1. Navigate to the **Describe with AI** configuration tab.
2. Type a thematic description into the box (e.g., *"A broken robot is trying to assemble itself"*).
3. Click the **Generate background** button to trigger processing.

<figure><img src="/files/qfS3j3H05g0dNSVSXE3O" alt=""><figcaption></figcaption></figure>

#### Method B: Manual Asset Uploads

If you already have designed error graphics ready for deploy:

1. Switch over to the **Upload image** configuration tab panel.
2. Drag and drop your file asset directly into the upload boundary box as illustrated in the screenshot.\
   **Asset Extension Rule:** All uploaded reference graphics must use standard PNG or JPG container formats.

<figure><img src="/files/3QThTU9iOU2bwVanBnek" alt=""><figcaption></figcaption></figure>

### Previewing, Removing, and Re-generating

Once your assets populate, they sync into the native device emulator viewport on the right edge of your dashboard, as seen in the screenshot.

<figure><img src="/files/qa6sa1vo7Obx2NZb1mPE" alt=""><figcaption></figcaption></figure>

* **Audit Aspect Layouts:** Verify that background focal points align correctly across standard mobile status bars and navigation panels.
* **Asset Deletion:** If a generated graphic context doesnt fit your design, click the red **Trash Can** icon. This wipes the canvas, letting you rewrite prompts or apply adjusted images.
* **Finalizing:** Press Next/Save to save state changes and commit your layout options.

### Continual Network Check Feature

For complex, single-page web applications (SPAs), we highly recommend enabling the Continual Network Check toggle setting found on your root screen dashboard page.

Why use this tool? Single-page web architectures do not naturally refresh or shift their URL endpoints during user actions. The Continual Network Check background service actively flags connection breaks without modifying or wiping out the page string position.

This critical tool prevents users from navigating deeper into stale cached app flows, filling out forms, or attempting database submissions while offline - saving them from permanently losing entered data.


# Style

Some bellows parameters can be updated with SDK later (check [Control Style & Colors](/guides/integration/control-style-and-colors) section)

[#app-background-color](#app-background-color "mention")

[#loader-color](#loader-color "mention")

[Style](/natively-platform/appearance/style#swipe-navigation)

[#pull-to-refresh](#pull-to-refresh "mention")

[#status-bar-style](#status-bar-style "mention")

[#safe-area](#safe-area "mention")

##

## Colors

### App Background Color

The top background (It displays only on iOS devices with notch)

If your page background is transparent, it will be the page's background.

![Top bar background (If iPhone with notch)](/files/YexLHOL7abzxGejH1MQ0)

### Loader Color

The color of a loader on the top (This loader will be displayed only while the page is loading)

![](/files/lhUSAo2g0rATtYKZhGyH)

## Device

### Swipe Navigation

Users can navigate between pages with a swipe gesture

![](/files/GVhmiYmyTblePZ2EdUZE)

### Pull To Refresh

Drag to the bottom to refresh the page

![](/files/CYlAZB0TVnGEpD1Tn8gw)

### Status Bar Style

In iOS: Status bar color, can be "Dark", "Light", or None(Hidden)

![](/files/Qb14Y0Gb2rD4aWiZhZgx)

In Android: "Dark" or "Light"

<figure><img src="/files/wySxBpXhTBR4ekQ0oljq" alt=""><figcaption><p>DARK</p></figcaption></figure>

<figure><img src="/files/FJ4WCLp8SuH81ROwFzyE" alt=""><figcaption><p>LIGHT</p></figcaption></figure>

### Safe Area <a href="#safe-area" id="safe-area"></a>

Safe Area - it's a space on the top of the app, that can be Enabled/Disabled.

{% hint style="info" %}
If it's Enabled, the background can be controlled by [App Background Color](#app-background-color)
{% endhint %}

<figure><img src="/files/iqewm5fqMreJAVgRzOXh" alt=""><figcaption><p>Disabled</p></figcaption></figure>

<figure><img src="/files/rqGo0530lifg3jB7wWVj" alt=""><figcaption><p>Enabled</p></figcaption></figure>

### Resize Viewport

Enabling this option adjusts the viewport, allowing users to scroll and access focused input fields that are obscured by the keyboard.

### Bottom Overlay

Enables Android navigation bar.

### Background Audio

Enable this feature if your app requires background audio playback.

{% hint style="warning" %}
If you enable this feature but your app doesn't require it for any functionality, your app may be rejected by Apple for: "The app declares support for audio in the UIBackgroundModes key in your Info.plist but we are unable to locate any features that require persistent audio". In such a case, you should disable the feature, save your changes, rebuild your app, and resubmit it for review.
{% endhint %}

### Wake Lock

Prevent your app from automatically locking the screen.&#x20;

{% hint style="info" %}
If it's Enabled, the feature can be controlled by [Wake Lock](https://docs.buildnatively.com/guides/integration/control-style-and-colors)
{% endhint %}


# Features

{% hint style="warning" %}
Changing any of the Native Features will require a rebuild, please make sure you enter the correct information.
{% endhint %}

{% content-ref url="/pages/XyPoKrdrCRoWmvVnb4l0" %}
[Bottom Bar](/natively-platform/features/bottom-bar)
{% endcontent-ref %}

{% content-ref url="/pages/L9ksSb5Bw9jNwYnx62gj" %}
[Deep Links](/natively-platform/features/deep-links)
{% endcontent-ref %}

{% content-ref url="/pages/c7u6h9YSdJWIGGdHsrNS" %}
[Geolocation](/natively-platform/features/geolocation)
{% endcontent-ref %}

{% content-ref url="/pages/mnz6BuzDoMPK7x60yfvJ" %}
[OneSignal Notifications](/hidden-pages/onesignal-notifications)
{% endcontent-ref %}

{% content-ref url="/pages/gQeCMP4MLArPHHkxHUkT" %}
[Contacts](/natively-platform/features/contacts)
{% endcontent-ref %}

{% content-ref url="/pages/vKnASxcSJbJg7hDUHujY" %}
[In-App Purchases](/natively-platform/features/purchases)
{% endcontent-ref %}

{% content-ref url="/pages/pSNkx9g8DoQcncuZELmo" %}
[HealthKit](/natively-platform/features/healthkit)
{% endcontent-ref %}

{% content-ref url="/pages/lxEV7uqZFHd1Ng6dX0yA" %}
[Admob](/hidden-pages/admob-1)
{% endcontent-ref %}

{% content-ref url="/pages/Y1HqajhJmeuW4ELmDJaW" %}
[Camera](/natively-platform/features/camera)
{% endcontent-ref %}

{% content-ref url="/pages/EQMsvsrOHljLefWu7kdB" %}
[Microphone](/natively-platform/features/microphone)
{% endcontent-ref %}

{% content-ref url="/pages/OrOm0Ur4kvbXpHr5hf3I" %}
[Analytics](/natively-platform/features/analytics)
{% endcontent-ref %}

{% content-ref url="/pages/yenLZP10BykrzHsFt6vt" %}
[Social Auth](/natively-platform/features/social-auth)
{% endcontent-ref %}

{% content-ref url="/pages/vqhO4kRQIiC0vxxBE0hh" %}
[NFC](/natively-platform/features/nfc)
{% endcontent-ref %}


# AdMob

Monetize your app with banner and interstitial ads through Google AdMob.

## What is AdMob?

Natively provides built-in support for [Google AdMob](https://admob.google.com/) to help you monetize your iOS and Android app through banner and interstitial ads. AdMob manages ad inventory, targeting, and delivery through its own console, and Natively handles displaying ads within your app.

{% hint style="warning" %}
Currently, Natively only supports **Banner** and **Interstitial** ad formats.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* A [Google AdMob](https://admob.google.com/) account.
* Your iOS and/or Android app [published](/natively-platform/app-info) in the Natively Dashboard.

## Google AdMob Configuration

### AdMob App configuration

{% stepper %}
{% step %}

#### Create app in AdMob

1. Log in to your [Google AdMob Console](https://admob.google.com/).
2. Navigate to **Apps** and click **Add Your First App** (or **Add App** if you already have other apps).
3. Select the platform - iOS or Android.
4. Indicate whether the app is already listed on a supported app store:
   * **Yes** - search for your app and click **Add**.
   * **No** - select **No**, enter your app name, and click **Add App**.

{% hint style="info" %}
If you're building for both platforms, repeat this process to create two separate apps in AdMob - one per platform.
{% endhint %}

<div><figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FbghTM56fRnog5GP5VizP%252F1.png%3Falt%3Dmedia%26token%3D12006427-c50d-47cd-a92f-12691e0adb87&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=efd0417e&#x26;sv=2" alt=""><figcaption><p>Step 1-2</p></figcaption></figure> <figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FFn1vu5cdBZEzUBh7rQ4z%252F2.png%3Falt%3Dmedia%26token%3D974e9819-acc6-41d7-ae08-b1443dcbf3bb&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=4cc18175&#x26;sv=2" alt=""><figcaption><p>Step 3-4</p></figcaption></figure> <figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FfCZ7rEfHsdu26KXqdQkW%252F3.png%3Falt%3Dmedia%26token%3D35263e8b-4701-40f2-9a67-d9737093a603&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=358fe58e&#x26;sv=2" alt=""><figcaption><p>Step 4 - Yes</p></figcaption></figure> <figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252F971PF2yUTe2LLwda6wYJ%252F3-1.png%3Falt%3Dmedia%26token%3Dcf1c7836-e896-493e-94bd-595a322563c4&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=7680e7aa&#x26;sv=2" alt=""><figcaption><p>Step 4 - No</p></figcaption></figure></div>
{% endstep %}

{% step %}

#### Retrieve AdMob app ID

1. Go to **Apps** > **View All Apps**.
2. Locate your newly created app.
3. Copy the **App ID** - it starts with `ca-app...` .

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FN5UUXc2kAcBXfHr9DcxK%252F5.png%3Falt%3Dmedia%26token%3D3b76dd72-0439-4f6b-a4f0-8cba4c834481&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9af11d44&#x26;sv=2" alt="" width="188"><figcaption><p>AdMob app ID</p></figcaption></figure>
{% endstep %}
{% endstepper %}

For AdMob's full setup walkthrough, see [Google's AdMob setup guide](https://support.google.com/admob/answer/9989980?hl=en).

### AdMob Ad Units configuration

For each ad format you plan to use - Banner and/or Interstitial - create a corresponding Ad Unit in AdMob.

1. Go to **Apps** > \[your app] > **Ad units**.
2. Click **Add ad unit**.
3. Select the ad format (**Banner** or **Interstitial**).
4. Name the ad unit and click **Create ad unit**.
5. Copy the generated **Ad unit ID**.

{% hint style="info" %}
Repeat for each format and platform you support. You'll use these **Ad Unit IDs** later - for **testing** on a registered device and for your **production** build once you're ready to release.
{% endhint %}

## Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **AdMob**.
2. Toggle the feature to **Enabled**.
3. Paste the **App IDs** you copied from AdMob into the respective fields:
   * **iOS App ID**
   * **Android App ID**
4. *(iOS only)* Enter a **Permission Description**.
5. Click **Save**.
6. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to iOS users when the OS asks them to grant tracking permission (App Tracking Transparency). Explain clearly why your app needs this permission - for example: "We use your data to provide personalized advertisements that are relevant to your interests." A vague description may result in App Store rejection.
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic (**Banner Ads**)

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Admob Banner

{% hint style="warning" %}
This element must be set to **Visible on page load** to initialize correctly. Place it directly on the page root - not inside Popups, Floating Groups, Group Focus elements, or Repeating Groups. To hide it from your UI, set its dimensions to 0x0 px.
{% endhint %}

#### Fields:

* **iOS UnitId** - your ad unit ID, or Google's test ad unit ID (`ca-app-pub-3940256099942544/2934735716`).
* **Android UnitId** - your ad unit ID, or Google's test ad unit ID (`ca-app-pub-3940256099942544/6300978111`).
* **Position** - `TOP` or `BOTTOM`.
* **Size Type** - `AUTO` or `CUSTOM`.
* **Banner Width** - applied only when **Size Type** is `CUSTOM`.
* **Banner Height** - applied only when **Size Type** is `CUSTOM`.
* **Preload Ad on Initialize** - loads the ad automatically on page load.
* **Show Ad on Initialize** - displays the ad automatically once loaded.

{% hint style="warning" %}
**Show Ad on Initialize** only takes effect if **Preload Ad on Initialize** is also enabled - showing an ad requires it to have loaded first. If preload is off, the ad will never auto-show, regardless of this setting.
{% endhint %}

#### Events:

* **Did finish setup** - fires when the banner has finished setting up and is ready to load an ad.
* **Did load ad** - fires when ad loading is finished, and the banner is ready to be displayed (called after **Load Ad** action).
* **Did fail to receive ad** - fires when loading the ad failed (can occur after **Load Ad** action).
* **Did record click** - fires when the user clicks on the banner.
* **Did record impression** - fires when the user sees the ad.
* **Did show banner** - fires when the banner is successfully shown.
* **Did hide banner** - fires when the banner is successfully hidden.

#### States:

* **Banner Is Ready** - Yes/No. Whether the banner has an ad ready to display.
* **Banner Is Visible** - Yes/No. Whether the banner is currently shown.
* **Ad Is Loaded** - Yes/No. Whether ad loading has finished.
* **Latest Error Message** - text description of the last error.
* **Latest Event** - the latest event code received from the app:
  * `DID_FINISH_SETUP` - the banner finished setting up and is ready to load an ad.
  * `DID_LOAD_AD` - ad loading finished, and the banner is ready to be displayed.
  * `DID_FAIL_TO_RECEIVE_AD` - loading the ad failed.
  * `DID_RECORD_CLICK` - the user clicked on the banner.
  * `DID_RECORD_IMPRESSION` - the user saw the ad.
  * `DID_SHOW_BANNER` - the banner was successfully shown.
  * `DID_HIDE_BANNER` - the banner was successfully hidden.

#### Actions:

* **Show Banner** - displays the banner on the screen.
* **Hide Banner** - removes the banner from the screen.
* **Load Ad** - manually fetches a new ad.
* **Check Banner Visible** - refreshes the **Banner Is Visible** state.
* **Check Banner Ready** - refreshes the **Banner Is Ready** state
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY ADMOB BANNER - DOCUMENTATION & EXAMPLES
// ============================================================================

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// new NativelyAdmobBanner(config, setupCallback, preloadAd, preloadCallback, showAd, showCallback)
//   - Initializes and sets up the banner.
//   - config.androidUnitId / config.iOSUnitId: string — defaults to Google's test IDs if omitted.
//   - config.position: string — "TOP" or "BOTTOM". Default "BOTTOM".
//   - config.sizeType: string — "AUTO" or "CUSTOM". Default "AUTO".
//   - config.custom_width / config.custom_height: number — used only if sizeType is "CUSTOM".
//   - preloadAd: boolean — automatically loads an ad after setup. Default false.
//   - showAd: boolean — automatically shows the ad after a successful load.
//     Only takes effect if preloadAd is also true. Default false.
//
// banner.loadAd(callback)
//   - Manually fetches a new ad from the AdMob network.
//
// banner.showBanner(callback)
//   - Makes the banner visible on the screen.
//
// banner.hideBanner(callback)
//   - Hides the banner from the screen (does not destroy the instance).
//
// banner.bannerIsReady(callback)
//   - Checks if an ad has been successfully loaded and is ready to show.
//
// banner.bannerIsVisible(callback)
//   - Checks if the banner is currently visible to the user.

// ============================================================================
// CALLBACK RESPONSE FIELDS
// ============================================================================
// resp.event: string — one of:
//   "DID_FINISH_SETUP"       — the banner finished setting up and is ready to load an ad.
//   "DID_SHOW_BANNER"        — the banner was successfully shown.
//   "DID_HIDE_BANNER"        — the banner was successfully hidden.
//   "DID_LOAD_AD"            — ad loading finished; the banner is ready to be displayed.
//   "DID_RECORD_CLICK"       — the user clicked on the banner.
//   "DID_FAIL_TO_RECEIVE_AD" — loading the ad failed.
//   "DID_RECORD_IMPRESSION"  — the user saw the ad.

// --- AdMob Banner. Quick Start (Automatic Handling) Example. Start ---
// Setting preloadAd and showAd to true handles loading and showing automatically.

// Google's official test ad unit IDs — safe to use during development,
// always return test ads. Replace with your own before release.
const androidUnitId = "ca-app-pub-3940256099942544/6300978111";
const iOSUnitId = "ca-app-pub-3940256099942544/2934735716";

const bannerConfig = {
  androidUnitId,
  iOSUnitId,
  position: "BOTTOM",
  sizeType: "AUTO"
};

const onBannerEvent = function (resp) {
  console.log("Banner event:", resp.event);
};

const preloadAd = true;
const showAd = true; // only works because preloadAd is also true

const banner = new NativelyAdmobBanner(
  bannerConfig,
  onBannerEvent,
  preloadAd,
  onBannerEvent,
  showAd,
  onBannerEvent
);

// --- AdMob Banner. Quick Start (Automatic Handling) Example. End ---


// --- AdMob Banner. Manual Control Example. Start ---
// Use this when you need to control exactly when the ad loads and shows.

const manualConfig = {
  androidUnitId, // Google's official test ID, defined above
  iOSUnitId,      // Google's official test ID, defined above
  position: "BOTTOM",
  sizeType: "AUTO"
};

const lifecycleHandler = function (resp) {
  if (resp.event === "DID_FINISH_SETUP") {
    manualBanner.loadAd();
  }
  if (resp.event === "DID_LOAD_AD") {
    manualBanner.showBanner();
  }
  if (resp.event === "DID_FAIL_TO_RECEIVE_AD") {
    console.warn("Ad failed to load.");
  }
};

const manualPreloadAd = false; // loading manually instead
const manualShowAd = false;    // showing manually instead

const manualBanner = new NativelyAdmobBanner(
  manualConfig,
  lifecycleHandler,
  manualPreloadAd,
  lifecycleHandler,
  manualShowAd,
  lifecycleHandler
);

// --- AdMob Banner. Manual Control Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively AdMob Banner SDK: new NativelyAdmobBanner(config, setupCallback, preloadAd, preloadCallback, showAd, showCallback) — config.androidUnitId/config.iOSUnitId: ad unit IDs, default to Google's official test IDs ("ca-app-pub-3940256099942544/6300978111" Android, "ca-app-pub-3940256099942544/2934735716" iOS) if omitted. config.position: "TOP" or "BOTTOM". config.sizeType: "AUTO" or "CUSTOM" (with config.custom_width/config.custom_height used only for CUSTOM). preloadAd: boolean, automatically loads the ad after setup. showAd: boolean, automatically shows the ad after a successful load — only takes effect if preloadAd is also true. banner.loadAd(callback), banner.showBanner(callback), banner.hideBanner(callback), banner.bannerIsReady(callback), banner.bannerIsVisible(callback) are available for manual control. All callbacks return resp.event: "DID_FINISH_SETUP" / "DID_SHOW_BANNER" / "DID_HIDE_BANNER" / "DID_LOAD_AD" / "DID_RECORD_CLICK" / "DID_FAIL_TO_RECEIVE_AD" / "DID_RECORD_IMPRESSION". For reference: https://docs.buildnatively.com/natively-platform/features/admob
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Show a banner ad at the bottom of the screen that loads and displays automatically".
{% endhint %}
{% endtab %}
{% endtabs %}

### Setup Logic (**Interstitial Ads**)

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Admob Interstitial

{% hint style="warning" %}
This element must be set to **Visible on page load** to initialize correctly. Place it directly on the page root - not inside Popups, Floating Groups, Group Focus elements, or Repeating Groups. To hide it from your UI, set its dimensions to 0x0 px.
{% endhint %}

{% hint style="warning" %}
Only one **Natively - Interstitial** element can exist per page. Avoid reloading the page frequently, since each reload re-initializes the ad.
{% endhint %}

#### Fields:

* **iOS UnitId** - your ad unit ID, or Google's test ID (`ca-app-pub-3940256099942544/4411468910`).
* **Android UnitId** - your ad unit ID, or Google's test ID (`ca-app-pub-3940256099942544/1033173712`).
* **Auto Ad Reload** - automatically loads a new ad after the **Did dismiss ad** event. You'll be notified once it's ready via the **Did Load Ad** event.

{% hint style="info" %}
An ad must be fetched again after each time it's shown. If **Auto Ad Reload** is enabled, this happens automatically - listen for **Did Load Ad**. If disabled, listen for **Did dismiss ad** and call **Load Ad** manually.
{% endhint %}

#### Events:

* **Did Load Ad** - fires when the ad view has finished setup and is ready to be displayed.
* **Did record click** - fires when the user clicks on the ad.
* **Did fail to present** - fires when presenting the ad failed (can occur after **Show Ad**).
* **Did fail to load ad** - fires when loading the ad failed (can occur after **Load Ad**).
* **Did show ad** - fires when the ad view is displayed to the user.
* **Did dismiss ad** - fires when the user dismisses (closes) the ad view.
* **Did record impression** - fires when the user sees the ad.

#### States:

* **Interstitial Is Ready** - Yes/No. Whether the ad view is ready to be displayed.
* **Latest Error Message** - text description of the last error.
* **Latest Event** - the latest event code received from the app:
  * `DID_FAIL_TO_LOAD_AD` - loading the ad failed.
  * `DID_LOAD_AD` - the ad finished loading and is ready to display.
  * `DID_SHOW_AD` - the ad was displayed to the user.
  * `DID_RECORD_CLICK` - the user clicked on the ad.
  * `DID_FAIL_TO_PRESENT` - presenting the ad failed.
  * `DID_DISMISS_AD` - the user closed the ad view.
  * `DID_RECORD_IMPRESSION` - the user saw the ad.

#### Actions:

* **Show Ad** - displays the interstitial ad.
* **Load Ad** - loads a new ad manually.
* **Check Interstitial Ready** - checks whether the ad view is active and can be displayed.
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY ADMOB INTERSTITIAL - DOCUMENTATION & EXAMPLES
// ============================================================================

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// new NativelyAdmobInterstitial(iOSUnitId, androidUnitId, setupCallback, autoAdReload, autoAdReloadCallback)
//   - Initializes the interstitial and loads the first ad automatically.
//   - iOSUnitId / androidUnitId: string — defaults to Google's test IDs if omitted.
//   - autoAdReload: boolean — automatically loads a new ad after the user
//     dismisses the current one. Default false.
//
// interstitial.loadAd(callback)
//   - Manually fetches a new ad from the network.
//
// interstitial.showInterstitialAd(callback)
//   - Displays the ad over the current screen.
//
// interstitial.interstitialIsReady(callback)
//   - Checks if an ad is currently loaded and ready to display.

// ============================================================================
// CALLBACK RESPONSE FIELDS
// ============================================================================
// resp.event: string — one of:
//   "DID_FAIL_TO_LOAD_AD" — loading the ad failed.
//   "DID_LOAD_AD"         — the ad finished loading and is ready to display.
//   "DID_SHOW_AD"         — the ad was displayed to the user.
//   "DID_RECORD_CLICK"    — the user clicked on the ad.
//   "DID_FAIL_TO_PRESENT" — presenting the ad failed.
//   "DID_DISMISS_AD"      — the user closed the ad view.
//   "DID_RECORD_IMPRESSION" — the user saw the ad.

// --- AdMob Interstitial. Standard Usage (Auto-Reload) Example. Start ---
// The SDK automatically fetches the next ad as soon as the current one is closed.

// Google's official test ad unit IDs — safe to use during development,
// always return test ads. Replace with your own before release.
const iOSUnitId = "ca-app-pub-3940256099942544/1033173712";
const androidUnitId = "ca-app-pub-3940256099942544/4411468910";

const onInterstitialEvent = function (resp) {
  console.log("Interstitial event:", resp.event);

  if (resp.event === "DID_LOAD_AD") {
    console.log("Ad ready — enable your Show Ad button now.");
  }
  if (resp.event === "DID_DISMISS_AD") {
    console.log("User closed the ad. No need to call loadAd() — auto-reload handles it.");
  }
};

const autoReloadAd = true;

const interstitial = new NativelyAdmobInterstitial(
  iOSUnitId,
  androidUnitId,
  onInterstitialEvent,
  autoReloadAd,
  onInterstitialEvent
);

function triggerLevelCompleteAd() {
  interstitial.interstitialIsReady(function (resp) {
    if (resp.status) {
      interstitial.showInterstitialAd();
    } else {
      console.log("Ad not ready yet, skipping...");
    }
  });
}

// --- AdMob Interstitial. Standard Usage (Auto-Reload) Example. End ---


// --- AdMob Interstitial. Manual Control Example. Start ---
// Use this if you need strict control over when ads are loaded.

const manualHandler = function (resp) {
  if (resp.event === "DID_LOAD_AD") {
    console.log("Manual ad loaded. Ready to show.");
  }
  if (resp.event === "DID_DISMISS_AD") {
    console.log("Ad closed.");
    // Optionally load the next one immediately:
    // manualInterstitial.loadAd();
  }
};

const manualAutoReload = false; // loading manually instead

const manualInterstitial = new NativelyAdmobInterstitial(
  iOSUnitId,     // Google's official test ID, defined above
  androidUnitId, // Google's official test ID, defined above
  manualHandler,
  manualAutoReload,
  manualHandler
);

function prepareAd() {
  console.log("Pre-loading ad...");
  manualInterstitial.loadAd();
}

// --- AdMob Interstitial. Manual Control Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively AdMob Interstitial SDK: new NativelyAdmobInterstitial(iOSUnitId, androidUnitId, setupCallback, autoAdReload, autoAdReloadCallback) — only one instance can be used per page. iOSUnitId/androidUnitId: ad unit IDs, default to Google's official test IDs ("ca-app-pub-3940256099942544/1033173712" iOS, "ca-app-pub-3940256099942544/4411468910" Android) if omitted. autoAdReload: boolean, automatically loads a new ad after the user dismisses the current one. interstitial.loadAd(callback), interstitial.showInterstitialAd(callback), interstitial.interstitialIsReady(callback) are available for manual control. All callbacks return resp.event: "DID_FAIL_TO_LOAD_AD" / "DID_LOAD_AD" / "DID_SHOW_AD" / "DID_RECORD_CLICK" / "DID_FAIL_TO_PRESENT" / "DID_DISMISS_AD" / "DID_RECORD_IMPRESSION". For reference: https://docs.buildnatively.com/natively-platform/features/admob
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Show an interstitial ad when the user completes a level, with automatic ad reloading".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Remember the Preload/Show dependency for Banner

`showAd` (or **Show Ad on Initialize** in Bubble) only takes effect if `preloadAd` is also enabled - showing requires the ad to have already loaded. If you want manual control, disable both and call `loadAd()` then `showBanner()` yourself.

#### Always reload Interstitial ads after they're shown

An interstitial ad is single-use - once dismissed, you need a new one. Enable auto-reload (**Auto Ad Reload** in Bubble, or the `autoAdReload` parameter in the JS SDK) to have this happen automatically, or manually call `loadAd()` after the ad is dismissed.

#### Only one Interstitial instance per page

Creating multiple `NativelyAdmobInterstitial` instances on the same page - or reloading the page frequently while one is active - can cause unexpected behavior.

#### Check readiness before showing an ad

Call `bannerIsReady()` or `interstitialIsReady()` (or the corresponding **Check Banner Ready** / **Check Interstitial Ready** action in Bubble) before showing an ad, to avoid trying to display one that hasn't finished loading yet.

## Testing

{% hint style="danger" %}
Never click your own **live** ads - this violates AdMob policy. Choose one of the two approaches below, depending on what you're testing.
{% endhint %}

### Quick testing with sample ads

Use Google's official test unit IDs - they always return test ads, regardless of whether the device is registered as a test device. Safe to use on any device without violating AdMob policy.

#### Banner:

* iOS - `ca-app-pub-3940256099942544/2934735716` .
* Android - `ca-app-pub-3940256099942544/6300978111` .

#### Interstitial:

* iOS - `ca-app-pub-3940256099942544/1033173712` .
* Android - `ca-app-pub-3940256099942544/4411468910` .

### Test device for live ads

If you want to preview how your actual, real ad units will render before going live, register your device in AdMob, then use your real ad unit IDs while testing on that device. Registered test devices see real ad content marked as a test, without risking policy violations from real clicks or impressions.

1. In AdMob, go to **Settings** > **Test devices**.
2. Click **Add Test Device**.
3. Enter your device name and [Advertising ID/IDFA](https://support.google.com/admob/answer/9691433?hl=en#ID).
4. In your Bubble plugin fields or JavaScript code, replace the sample test unit IDs with your real **Ad Unit IDs** - created in the [AdMob Ad Units configuration](#admob-a-d-units-configuration) above.

{% hint style="warning" %}
Real ad units will still serve real ads to any device that isn't registered as a test device - only use your real IDs on a registered test device until you're ready to release.
{% endhint %}

<div><figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FPbHn3rdwJJ1XuQsfqQ18%252Ftesting-1.png%3Falt%3Dmedia%26token%3D0ac51312-5567-4b8c-8bbe-a225a4f69cd3&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=918d8072&#x26;sv=2" alt=""><figcaption><p>Steps 1-2</p></figcaption></figure> <figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FJScTyUEhmY8Cf1lBS3Y8%252Ftesting-2.png%3Falt%3Dmedia%26token%3D91770901-c594-41c3-ab02-6390c32de47d&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=5620ef44&#x26;sv=2" alt=""><figcaption><p>Step 3</p></figcaption></figure></div>

For AdMob's full app testing guides, see [iOS testing](https://developers.google.com/admob/ios/test-ads) and [Android testing](https://developers.google.com/admob/android/next-gen/test-ads).

## Release

Once your ads are tested and behaving as expected, the last steps are to switch to live ad units, publish your app, and formally connect it to AdMob - without this final connection, your app will not serve real ads even after it's live in the stores.

{% stepper %}
{% step %}

#### Switch to live ad units

Swap the test IDs in your Bubble plugin fields or JavaScript code for your real **Ad Unit IDs**, created in the [AdMob Ad Units configuration](#admob-a-d-units-configuration) above.

{% hint style="warning" %}
Double-check you've replaced test IDs in every **Banner** and **Interstitial** element or SDK call before submitting - a build shipped with test IDs will only ever serve test ads to real users.
{% endhint %}
{% endstep %}

{% step %}

#### Submit and publish your app

Submit your app to the App Store and Google Play as you normally would. See [Testing & Submitting your app](/guides/testing-and-submitting-your-app) for the full release process.
{% endstep %}

{% step %}

#### Link your live listing to AdMob

Once your app is approved and published on the App Store and/or Google Play:

1. Go to **Apps** > \[your app] > **App Settings** in AdMob.
2. Click **Add Store**.
3. Connect your App Store and/or Google Play listing.

{% hint style="info" %}
This step is required for real ads to serve. Review can take about a day - during that window, ad requests from your live app may return no fill or fall back to test behavior.
{% endhint %}

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FClJBqizoXseptHyMr1uM%252Fimage.png%3Falt%3Dmedia%26token%3Df22d9adc-3baa-4ba7-9e56-aaa22e5bcead&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=b101fc79&#x26;sv=2" alt="" width="375"><figcaption><p>Step 2</p></figcaption></figure>
{% endstep %}
{% endstepper %}

## Troubleshooting

<details>

<summary>No ads are showing, even with test unit IDs</summary>

Confirm the feature is enabled and your App IDs are correctly entered under **Features** > **AdMob**, then confirm the app has been rebuilt - AdMob configuration changes only take effect after a rebuild

</details>

<details>

<summary>Banner never appears, even though <code>showAd</code> / <strong>Show Ad on Initialize</strong> is enabled</summary>

Confirm `preloadAd` (or **Preload Ad on Initialize**) is also enabled - showing an ad only happens automatically if the ad has been preloaded first. If you want manual control instead, disable both and call `loadAd()` followed by `showBanner()` yourself.

</details>

<details>

<summary>Interstitial ad fails to show, or <code>showInterstitialAd()</code> does nothing</summary>

Confirm `interstitialIsReady()` returns `true` before calling `showInterstitialAd()`. Showing an ad that hasn't finished loading fails silently or triggers `DID_FAIL_TO_PRESENT` .

</details>

<details>

<summary>No new interstitial ad appears after the previous one was dismissed</summary>

An interstitial ad is single-use. Confirm **Auto Ad Reload** / `autoAdReload` is enabled, or manually call `loadAd()` after `DID_DISMISS_AD`.

</details>

<details>

<summary>Multiple Interstitial ads misbehave, or events fire unexpectedly</summary>

Only one `NativelyAdmobInterstitial` instance is supported per page. Check that you haven't created more than one, and avoid reloading the page frequently while an interstitial is active.

</details>

<details>

<summary>Real ads don't show after publishing</summary>

Confirm you've linked your live App Store / Google Play listing to AdMob under **Apps** > **\[Your App** > **App Settings** > **Add Store**. This step is required for real ads to serve and can take about a day to process.

</details>

<details>

<summary>App rejected by Apple for vague tracking permission text</summary>

The **Permission Description** (iOS only) must clearly explain why your app requests tracking. Avoid vague text like "to improve the app" - be specific, e.g., "We use your data to provide personalized advertisements that are relevant to your interests."

</details>

<details>

<summary>Not working in a web browser or the Natively Preview app</summary>

AdMob is a native feature and does not work in a standard web browser or in the Natively Preview app. Test on a real device using a full Natively build.

</details>

[^1]: Replace this placeholder


# Analytics

Track app installs, attribute campaigns, and send custom events to your analytics provider.

## What is Analytics?

Analytics lets you measure how users find and use your app - tracking installs, attributing them to specific marketing campaigns, and sending custom events to understand user behavior.

## Supported providers

**AppsFlyer** - a dedicated mobile attribution and marketing analytics platform, focused on campaign tracking, conversion attribution, and audience segmentation.

{% content-ref url="/pages/YCD3PTjpSWTcrpjU28zJ" %}
[AppsFlyer](/natively-platform/features/analytics/appsflyer)
{% endcontent-ref %}

**Facebook** - Meta's analytics and advertising platform, useful if you're already running Facebook or Instagram ad campaigns and want conversion tracking tied directly to them.

{% content-ref url="/pages/jUlFsWmnQBWWUOYQUnI6" %}
[Facebook](/natively-platform/features/analytics/facebook)
{% endcontent-ref %}


# AppsFlyer

Attribute app installs and in-app activity to your marketing campaigns using AppsFlyer.

## What is AppsFlyer?

AppsFlyer is a mobile attribution and marketing analytics platform that helps you understand where your app installs come from and how users move through their journey after installing. It supports campaign management, conversion attribution, audience segmentation, and retention tracking.

{% hint style="info" %}
iOS attribution via **SKAdNetwork** is supported, letting ad campaigns be linked to iPhone installs in compliance with Apple's attribution requirements.
{% endhint %}

{% hint style="danger" %}
If you have In-App Purchases enabled, AppsFlyer automatically tracks them - this cannot be disabled.
{% endhint %}

## Prerequisits

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* An [AppsFlyer](https://www.appsflyer.com/start/) account.
* Your iOS and/or Android app [published](/natively-platform/app-info) in the Natively Dashboard.

## AppsFlyer Configuration

{% tabs %}
{% tab title="Android" %}

1. Go to [My Apps](https://hq1.appsflyer.com/apps/myapps) in AppsFlyer and click **Add app**.
2. Select the **Android, AndroidTV, Fire** platform.
3. Indicate your app's Google Play status:
   * **In Store** - select this and enter your app's Google Play URL.
   * **Pending approval/not published** - select this and enter your **Package ID**, found in the Natively Dashboard under **Publish** > **Android Build** > **Bundle Identifier**.
4. Select your app's currency, and whether it's targeted at a kids audience.
5. On the success screen, copy the generated **Dev Key**.

{% hint style="warning" %}
The **Dev Key** is the same for both iOS and Android - if you support both platforms, you only need to enter it once, but you must rebuild each app separately.
{% endhint %}

<div><figure><img src="/files/gssf2rCRYv9OByTIMPe1" alt="" width="188"><figcaption><p>Step 1</p></figcaption></figure> <figure><img src="/files/vYm5tVseDVb8BJobpBWC" alt="" width="188"><figcaption><p>Step 2-3</p></figcaption></figure> <figure><img src="/files/Aizj0K3keXunIfRmJgLv" alt="" width="188"><figcaption><p>Step 4</p></figcaption></figure> <figure><img src="/files/iuWZFGrxJ9ajHmuxGRCK" alt="" width="188"><figcaption><p>Step 5</p></figcaption></figure></div>
{% endtab %}

{% tab title="iOS" %}

1. Go to [My Apps](https://hq1.appsflyer.com/apps/myapps) in AppsFlyer and click **Add app**.
2. Select the **iOS, tvOS, MacOS** platform.
3. Indicate your app's App Store status:
   * **In Store** - select this and search for your published app.
   * **Pending approval/not published** - select this if your app isn't live yet.
4. Select your store country, then enter your **App ID** - found in the Natively Dashboard under **Publish > iOS Build > App Store App ID**, or directly in App Store Connect.
5. Select your app's currency, and whether it's targeted at a kids audience.
6. On the success screen, copy the generated **Dev Key**.

{% hint style="warning" %}
The **Dev Key** is the same for both iOS and Android - if you support both platforms, you only need to enter it once, but you must rebuild each app separately.
{% endhint %}

<div><figure><img src="/files/CAaiY1v58SgSuSwTp9xY" alt="" width="188"><figcaption><p>Step 1</p></figcaption></figure> <figure><img src="/files/rUkdne9A5gPWv9vmIsoX" alt="" width="188"><figcaption><p>Step 2-4</p></figcaption></figure> <figure><img src="/files/TDlyiJ9uP6lhzgT3ZjGs" alt="" width="188"><figcaption><p>Step 5</p></figcaption></figure> <figure><img src="/files/PZhqt1KHCRJgSrl1oAYL" alt="" width="188"><figcaption><p>Step 6</p></figcaption></figure></div>
{% endtab %}
{% endtabs %}

## Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **Analytics** > **AppsFlyer**.
2. Toggle the feature to **Enabled**.
3. Enter the **Dev Key** you copied from AppsFlyer.
4. *(iOS only)* Enter the **Permission Description**.
5. Click **Save**.
6. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to iOS users when the OS asks them to grant tracking permission (App Tracking Transparency). This field only applies to iOS. Explain clearly why your app needs this permission - for example: "We use your data to provide personalized advertisements that are relevant to your interests."
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Natively doesn't expose a dedicated AppsFlyer-specific method - app install tracking and attribution happen automatically once configured, with no additional call required.

For any event beyond the automatic install event - such as `registration_completed` or `profile_completed` - you must send it yourself using [Custom Event](/natively-platform/features/analytics/custom-event). These events **cannot** be configured entirely within the AppsFlyer or Meta dashboard; the dashboards can map, report, and optimize on events, but your app must actually send each event at the moment the action happens.

{% content-ref url="/pages/bhZtyuAB014fEBKEjDEi" %}
[Custom Event](/natively-platform/features/analytics/custom-event)
{% endcontent-ref %}

### How to use

{% hint style="warning" %}
AppsFlyer's dashboard data isn't real-time. Standard dashboard data typically takes 8–20 hours to appear. iOS attribution via SKAdNetwork is delayed further - installs are reported 72–120 hours after first app open, and up to 13 days for Google Ads specifically. Don't assume a missing install or event is broken until you've allowed for this delay.
{% endhint %}

#### Track installs automatically, no setup needed

Once configured, AppsFlyer tracks app installs and attribution on its own - you don't need to call any method for this to work.

#### Use Custom Event for everything beyond installs

For engagement, feature usage, or funnel tracking, use [Custom Event](/natively-platform/features/analytics/custom-event) - it's forwarded to AppsFlyer automatically once configured.

#### Verify your integration before launching campaigns

Use AppsFlyer's [SDK Integration Test](https://support.appsflyer.com/hc/en-us/articles/360001559405-Testing-the-SDK-integration-for-marketers) tool (see [Testing](/natively-platform/features/analytics/custom-event#testing) on the Custom Event page) to confirm installs and events are actually reaching AppsFlyer before you spend budget attributing campaigns to it.

## Troubleshooting

<details>

<summary>Installs aren't showing up in AppsFlyer</summary>

Go through this checklist:

* Confirm the feature is enabled and the Dev Key is correcly entered in the Natively Dashboard.
* Confirm the app has been rebuilt.
* Confirm you've allowed enough time for data to propagate.

</details>

<details>

<summary>Installs or events stop appearing after working previously</summary>

Check whether your AppsFlyer **Welcome Package** has expired or been exhausted. New AppsFlyer accounts get 12,000 free conversions usable within their first 12 months - once that window closes or the conversion limit is hit, tracking on the Zero plan may stop or require a plan upgrade. Check your plan status under **AppsFlyer** > **Account** > **Plan** before assuming the integration itself is broken.

</details>

<details>

<summary>Not working in a web browser or the Natively Preview app</summary>

This is a native feature and does not work in a standard web browser or in the Natively Preview app. Test on a real device using a full Natively build.

</details>


# Facebook

Track app installs and in-app activity, and measure ad performance for your Facebook and Instagram campaigns.

## What is Facebook Analytics?

Facebook (Meta) Analytics lets you track how users interact with your app and measure the performance of your Facebook and Instagram ad campaigns. It logs standard events automatically (app installs, sessions, purchases) and lets you send custom events to understand deeper engagement.

## Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* A [Facebook Developer](https://developers.facebook.com/) account.
* **iOS:**
  * Your iOS app is already [published](/natively-platform/app-info/ios-build) in the Natively Dashboard.
* **Android:**
  * Your Android app is already [published](/natively-platform/app-info/android-build) in the Natively Dashboard.
  * Your app is [uploaded](https://support.google.com/googleplay/android-developer/answer/9845334#zippy=%2Cinternal-test-manage-up-to-testers) to the **Google Play Console** - required to access SHA-1 signing certificate fingerprints.

## Facebook Configuration

{% stepper %}
{% step %}

### Create your Facebook app

1. Go to [My Apps](https://developers.facebook.com/apps/) in Meta for Developers and click **Create App**.
2. Select **None** as the app type.
3. Enter your **app name** and **contact email**.

<div><figure><img src="/files/maGo3q81idWqAY3uaVQF" alt="" width="188"><figcaption><p>Step 1</p></figcaption></figure> <figure><img src="/files/t1ACEPCXI8vMyNJ6GVsk" alt="" width="188"><figcaption><p>Step 2</p></figcaption></figure> <figure><img src="/files/DFrHqo9vJge1GhSRLprj" alt="" width="188"><figcaption><p>Step 3</p></figcaption></figure></div>
{% endstep %}

{% step %}

### Configure your platform(s)

1. In your app, go to **Settings** > **Basic**.
2. Scroll down and click **Add platform**.
3. Select **Android** or **iOS**, and click **Next**.

<div><figure><img src="/files/V9nhcrZUDoBIJupqwdVu" alt="" width="188"><figcaption><p>Step 1</p></figcaption></figure> <figure><img src="/files/fYDa4IznKl1eU3syM431" alt="" width="188"><figcaption><p>Step 2-3</p></figcaption></figure></div>

{% tabs %}
{% tab title="Android" %}
{% hint style="info" %}
Natively supports **Google Play**. Other Android app stores may work but are unsupported.
{% endhint %}

1. In **Add platform**, select **Google Play** from the list of stores.
2. Generate your **Key Hash**:
   1. Go to your **Google Play Console**, open your app's page, and navigate to **Protected With Play** > **Play Store protection** > **Protect app signing key** > **Manage Play App Signing**.
   2. Copy the **SHA-1 certificate fingerprint** under **Upload key certificate**.
   3. Go to a [Hex to Base64 converter](https://base64.guru/converter/encode/hex).
   4. Paste your SHA-1 value and click **Convert Hex to Base64**.
   5. Copy the resulting Base64 value.
   6. Repeat for the **App signing key certificate** SHA-1 fingerprint.
3. Paste both Base64 values into the **Key hashes** field.
4. Enter your **Package Name** - found in the Natively Dashboard under **Publish** > **Android Build** > **Bundle Identifier**.
5. Enter your **Class Name**: `{YOUR_ANDROID_APP_BUNDLE_ID}.MainActivity` - e.g. `com.example.natively.MainActivity`
6. Click **Save changes**.
7. Go to **Settings** > **Advanced**, and copy your **Client Token** and **App ID**.

{% hint style="warning" %}
The App ID and Client Token are the same for iOS and Android - if you support both platforms, you only need to enter them once, but you must rebuild each app separately.
{% endhint %}

<div><figure><img src="/files/4auZlfwilmhNVgVyH3be" alt="" width="188"><figcaption><p>Step 1</p></figcaption></figure> <figure><img src="/files/ReoNA0pNzW0p3SHHztJc" alt="" width="188"><figcaption><p>Step 2.a</p></figcaption></figure> <figure><img src="/files/y3t4hsfdD3HXIEFVr62V" alt="" width="188"><figcaption><p>Step 2.b, 2.f</p></figcaption></figure> <figure><img src="/files/epr5k4CsRSZEWagk6Yp6" alt="" width="188"><figcaption><p>Step 2.c-2.e</p></figcaption></figure> <figure><img src="/files/ndUf5KYZOXwb61CuDjsd" alt="" width="188"><figcaption><p>Step 3</p></figcaption></figure> <figure><img src="/files/4hbqVfsEXGaygcg1i6ZA" alt="" width="188"><figcaption><p>Step 4-5</p></figcaption></figure> <figure><img src="/files/ffKlOc1QPJxeXnXcUPtd" alt="" width="188"><figcaption><p>Step 6-7</p></figcaption></figure></div>
{% endtab %}

{% tab title="iOS" %}

1. Enter your **Bundle ID** - found in the Natively Dashboard under **Publish** > **iOS Build** > **Bundle Identifier**.
2. Enter your **iPhone/iPad Store ID** - found in the Natively Dashboard under **Publish** > **iOS Build** > **App Store App ID**. This value is the same for iPhone and iPad (if [iPad Support](/natively-platform/settings#ipad-support-only-for-ios) is enabled).
3. Click **Save changes**.
4. Go to **Settings** > **Advanced**, and copy your **Client Token** and **App ID**.

{% hint style="warning" %}
The App ID and Client Token are the same for iOS and Android - if you support both platforms, you only need to enter them once, but you must rebuild each app separately.
{% endhint %}

<div><figure><img src="/files/F7ZdNdvITRl6d8NLzXa4" alt="" width="188"><figcaption><p>Step 1-3</p></figcaption></figure> <figure><img src="/files/s5epe296NKHfAA1J5Emr" alt="" width="188"><figcaption><p>Step 4</p></figcaption></figure></div>
{% endtab %}
{% endtabs %}
{% endstep %}
{% endstepper %}

## Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **Analytics** > **Facebook**.
2. Toggle the feature to **Enabled**.
3. Enter the **App ID** and **Client Token** you copied from Facebook.
4. *(iOS only)* Enter the **Permission Description**.
5. Click **Save**.
6. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to iOS users when the OS asks them to grant tracking permission (App Tracking Transparency). This field only applies to iOS. Explain clearly why your app needs this permission - for example: "We use your data to provide personalized advertisements that are relevant to your interests".
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Natively doesn't expose a dedicated Facebook-specific method - standard events (app installs, sessions, and purchases) are logged automatically once configured, with no additional call required.

For any event beyond the automatic ones - such as `registration_completed` or `viewed_product` - send it yourself using [Custom Event](/natively-platform/features/analytics/custom-event). These events cannot be configured entirely within the Facebook Events Manager; the dashboard can map, report, and optimize on events, but your app must actually send each event at the moment the action happens.

{% content-ref url="/pages/bhZtyuAB014fEBKEjDEi" %}
[Custom Event](/natively-platform/features/analytics/custom-event)
{% endcontent-ref %}

### How to use

{% hint style="warning" %}
Facebook's Events Manager data isn't real-time. Standard events typically appear within seconds to minutes, but delivery timing can vary - Facebook's own documentation notes it can take up to 30 minutes in some cases. Don't assume a missing event is broken until you've allowed for this delay.
{% endhint %}

#### Standard events are logged automatically; no setup needed

Once configured, app installs, sessions, and in-app purchases are tracked without any additional call.

#### Use Custom Event for everything beyond standard events

For engagement, feature usage, or funnel tracking, use [Custom Event](/natively-platform/features/analytics/custom-event) - it's forwarded to Facebook automatically once configured.

#### Stick to Facebook's event name limit

Facebook allows up to 1,000 distinct event names per app - reuse a small, consistent set of event names rather than generating dynamic ones per event.

#### Verify your integration before launching campaigns

Use Facebook's Test Events tool (see [Testing](/natively-platform/features/analytics/custom-event#facebook) on the Custom Event page) to confirm events are actually reaching Facebook before you spend budget attributing campaigns to it.

## Troubleshooting

<details>

<summary>Standard events aren't showing up on Facebook</summary>

Go through this checklist:

* Confirm the feature is enabled and both the App ID and Client Token are entered correctly in the Natively Dashboard.
* Confirm the app has been rebuilt.
* Confirm you've allowed enough time for data to propagate.

</details>

<details>

<summary>iOS setup fails, or the app isn't recognized</summary>

Confirm your **Bundle ID** and **iPhone/iPad Store ID** exactly match what's configured in the Natively Dashboard.

</details>

<details>

<summary>Android setup fails, or the app isn't recognized</summary>

Confirm both Key Hash values (Upload key and App signing key) were correctly converted from SHA-1 to Base64, and that your **Package Name** and **Class Name** exactly match your app's configuration.

</details>

<details>

<summary>Not working in a web browser or the Natively Preview app</summary>

This is a native feature and does not work in a standard web browser or in the Natively Preview app. Test on a real device using a full Natively build.

</details>


# Custom Event

Send a custom event with optional data to your configured Analytics provider - AppsFlyer or Facebook.

## Prerequisites

* At least one supported analytics provider must be configured: [AppsFlyer](/natively-platform/features/analytics/appsflyer) or [Facebook](/natively-platform/features/analytics/facebook).

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Action] Natively - Send Custom Event

* **Event name** - the identifier that will appear in your provider's dashboard (e.g. `user_onboarded`, `checkout_started`).
* **Event data** - a JSON object containing custom parameters and values to send with the event.

<figure><img src="/files/Pm0OEYQFKyCJIVhJ5zJM" alt="" width="375"><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY ANALYTICS SDK - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — analyticsTrackEvent is available directly
// on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.analyticsTrackEvent(name, data)
//   - Logs an analytics event with optional event data.
//   - Forwarded to your configured provider: AppsFlyer or Facebook.
//   - name: string — the event identifier
//   - data: object — plain JS object of custom parameters and values. Optional.

// --- Custom Event. Example. Start ---

const eventData = {
  user_id: "12345",
  screen: "home",
  duration_seconds: 45,
  is_premium_user: true,
};

window.natively.analyticsTrackEvent("button_clicked", eventData);

// --- Custom Event. Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Analytics SDK: window.natively.analyticsTrackEvent(name, data) logs an analytics event, forwarded to your configured provider (AppsFlyer or Facebook) — name: string, the event identifier; data: plain object, optional, custom parameters and values. No callback required. For reference: https://docs.buildnatively.com/natively-platform/features/analytics/custom-event
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Track a custom event when the user completes onboarding, including their user ID and signup method".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Use event names that match your provider's conventions

AppsFlyer and Facebook both expect consistent, descriptive event names - stick to `snake_case` or a naming scheme you use consistently across your app (e.g. `signed_up`, `completed_purchase`, `viewed_product`), so events are easy to filter and analyze in your provider's dashboard.

#### Only include data relevant to that event

`data` accepts any plain object, but keep it scoped to what actually describes the event - a `checkout_started` event might include `cart_value` and `item_count`, but doesn't need unrelated user profile data.

#### Track key user milestones, not every interaction

Custom events are most useful for meaningful actions - signups, purchases, feature adoption - rather than logging every tap or scroll, which adds noise without improving attribution or analysis.

#### Combine with In-App Purchases tracking; don't duplicate it

If you have In-App Purchases enabled, AppsFlyer already tracks purchases automatically - you don't need a custom event for that. Use custom events for everything else: onboarding steps, feature usage, engagement milestones.

## Testing

{% tabs %}
{% tab title="AppsFlyer" %}

1. Add your test device to your AppsFlyer account. See [AppsFlyer's Registering test devices](https://support.appsflyer.com/hc/en-us/articles/207031996-Registering-test-devices#register-a-device-manually).
2. In the AppsFlyer dashboard, go to **Settings** > **SDK Integration Test** > **Live Events**.
3. Select your app and your test device, then click **Continue**.
4. Click **Start** to begin listening for live events.
5. Launch the app on your test device and trigger the custom event you want to test (e.g., tap the button that calls `analyticsTrackEvent`).
6. Check the **Live Events** dashboard - your event and its associated data should appear there in real time.

<div><figure><img src="/files/rJrmIBnC8EshAJslwRQD" alt="" width="188"><figcaption><p>Step 2</p></figcaption></figure> <figure><img src="/files/aZwqu4b12H0tbF9cyaML" alt="" width="137"><figcaption><p>Step 3</p></figcaption></figure> <figure><img src="/files/BFy9psuP7M1yscvWe8uT" alt="" width="188"><figcaption><p>Step 4</p></figcaption></figure> <figure><img src="/files/IeJJZvZqcxkOhoziTDpI" alt="" width="188"><figcaption><p>Result</p></figcaption></figure></div>
{% endtab %}

{% tab title="Facebook" %}

1. Go to your app's [Events Manager](https://business.facebook.com/events_manager) in Meta Business Suite, and select **Test Events**.
2. Pair your test device - Facebook provides a QR code or pairing code to scan/enter on the device running your app.
3. Launch the app on your test device and trigger the custom event you want to test.
4. Check the **Test Events** dashboard for your event and its parameters.

{% hint style="info" %}
Event delivery timing can vary - some events appear within seconds, though Facebook's own documentation notes it can take up to 30 minutes in some cases.
{% endhint %}

For the full setup process, see [Meta's App Events documentation](https://developers.facebook.com/docs/app-events/overview/).
{% endtab %}
{% endtabs %}

## Troubleshooting

<details>

<summary>Custom event doesn't appear in AppsFlyer's Live Events</summary>

Confirm you clicked **Start** on the SDK Integration Test page before triggering the event - Live Events only captures events sent after listening has started. Also confirm the app has been rebuilt after enabling AppsFlyer.

</details>

<details>

<summary>Custom event doesn't appear in Facebook's Test Events</summary>

Confirm your test device is correctly paired via the QR code or pairing code. Delivery timing can vary - allow up to 30 minutes before assuming it failed, per Facebook's own documentation.

</details>

<details>

<summary>The event appears, but without the expected data</summary>

`data` is a plain JavaScript object, not `undefined`, `null`, or a non-serializable value. Check the [Debug Console](/guides/integration/debug-console) to see exactly what payload was sent.

</details>

<details>

<summary>Event name doesn't show up as expected in the dashboard</summary>

Some providers cap the number of distinct event names (e.g. Facebook allows up to 1,000) - reusing a small, consistent set of event names is safer than generating dynamic ones per event.

</details>

<details>

<summary>Not working in a web browser or the Natively Preview app</summary>

This is a native feature and does not work in a standard web browser or in the Natively Preview app. Test on a real device using a full Natively build.

</details>

[^1]: Replace this placeholder


# Bottom Bar

### How the Bottom Bar feature works

The Bottom Bar automatically appears in your app upon launch when this option is enabled. If you link tabs to pages requiring login, logged-out users will encounter a standard workflow (e.g., redirection to a login page within the tab) instead of seeing the page content.

Tabs function like browser tabs, remembering their navigation state. If a user navigates from a tab's linked page to another page within the app, the tab will display the last visited page, not the original linked page. This behavior persists even when switching between tabs.

<figure><img src="/files/5C7k2OxiRrlHYbsGUw0x" alt=""><figcaption></figcaption></figure>

### Setting up the Bottom Bar

Turn on the **Bottom Bar** feature and set colors:

* Bottom Bar Background Color - sets the background color of the bar.
* Icon / Label Default Color - sets the color of inactive tab icons and labels.
* Icon / Label Active Color - sets the color of the active tab icon and label.
* Icon / Label Active Background Color - sets the background color of the active tab.

<figure><img src="/files/PIJW9XTC28unsyYWtgmj" alt=""><figcaption></figcaption></figure>

Configure **options**:

* Haptic Feedback - enables haptic feedback when tapping tabs.
* Show Icons - show or hide tab icons.
* Show Labels - show or hide tab labels.
* Show on launch - when this option is disabled, the bottom bar will not automatically appear when the app is launched. You can then display it later by using the 'Show bottom bar' action.

<figure><img src="/files/L06is1ogeVyksMZDpuXV" alt=""><figcaption></figcaption></figure>

### Configuring tabs

You can add up to 5 tabs to the bottom bar.

1. Enter a label in the input field and click 'Add'. This creates a new tab with customizable settings. The label will be enclosed in quotation marks automatically, but these won't be visible in your app.
2. Assign a number from 0 to 5 to determine the tab's position in the bar. Tabs are arranged based on these numbers.
3. Enter the URL the tab should link to. This URL will also be enclosed in quotation marks automatically, but they won't be visible in your app.
4. Provide an SVG icon for the tab.
5. Click 'Save Tab' to apply your changes.

{% hint style="info" %}
All fields (Label, Order, URL, and Icon) are required. If any field is missing or invalid, the tab will not appear in the Bottom Bar.
{% endhint %}

<figure><img src="/files/8G3dSasp52SskmKFqyiO" alt=""><figcaption></figcaption></figure>

### Edit tabs

You can change the Label, Order, URL, and Icon of each tab. Remember to save any changes by clicking the 'Save Tab' button.&#x20;

To change the icon, first delete the existing one by clicking the 'Trash' icon. This will reveal the 'Upload icon' button.

To delete a tab entirely, click the 'Delete Tab' button. You can then add a new tab if needed.

### Apply chages to the app

To apply the changes to your app, save the settings and rebuild your app.

### How to use the Bottom Bar?

{% content-ref url="/pages/FBhaKfkRKaOCH2PUwDgu" %}
[Bottom Bar](/guides/integration/bottom-bar)
{% endcontent-ref %}


# Calendars

Let users retrieve their device calendars and create new calendar events, directly from your app.

## What is Calendars?

The Calendars feature gives your app access to the device's native calendars, letting users retrieve their existing calendars or create new events without leaving the app. This is useful for features like adding a booking confirmation, an appointment, or a reminder directly to the user's calendar.

Accessing calendars requires the user to grant permission the first time the feature is used, on both iOS and Android.

## Prerequisites

{% hint style="success" %}
This feature requires any **paid** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* [Contacts](/natively-platform/features/contacts) feature is already enabled in the Natively Dashboard.

## Natively Dashboard Setup

{% hint style="warning" %}
Make sure [Contacts](/natively-platform/features/contacts) is already enabled; Calendars can't be enabled otherwise.
{% endhint %}

1. Open your Natively app dashboard and navigate to **Features** > **Calendars**.
2. Toggle the feature to **Enabled**.
3. Enter the **Permission Description**.
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to users when the OS asks them to grant **calendar** access. Explain clearly why your app needs this permission - for example: "We use your calendar to let you save appointments directly to your device".
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Calendar

{% hint style="danger" %}
Set the element's data type field to **CalendarObject (Natively - ...)**.
{% endhint %}

<figure><img src="/files/jk6OZcNA1cPOdi9JltLV" alt=""><figcaption></figcaption></figure>

#### Events:

* **Get Calendars Success** - fires when calendars are successfully retrieved.
* **Get Calendars Failed** - fires if retrieving calendars fails.
* **Create Event Success** - fires when a new event is successfully created.
* **Create Event Failed** - fires if creating an event fails.

#### States:

* **Get Calendars Result** - list of `CalendarObject`, each with:
  * **id** - unique calendar identifier on the device.
* **Create Event Calendar ID** - the calendar ID where the new event was created.
* **Create Event/Get Calendars Status** - result status after calling **Create Event** or **Get Calendars**.
* **Event Title** - the title of the event that was created.
* **Event End Date** - the event's end date, in ISO 8601 format.
* **Event Start Date** - the event's start date, in ISO 8601 format.
* **Error** - error message, if any. Examples:
  * `add_calendar_event_failure` - something went wrong on the app side;
  * `start_date_missing` ;
  * `end_date_missing` ;
  * `no_available_calendars` - the user has no available calendars;
  * `cannot_retrieve_calendars` - something went wrong on the app side;
  * `calendar_permission_missing` - permission not granted;
  * `timezone_missing` ;
* **Message** - describes the current stage of the process, if applicable.

#### Actions:

* **Get Calendars** - returns a list of `CalendarObject`
* **Create Event** - returns the event's Calendar ID, Title, End Date, and Start Date:
  * **Title**;
  * **End Date**;
  * **Start Date**;
  * **Timezone** - ISO 8601 format;
  * **Description**.<br>
    {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY CALENDARS - DOCUMENTATION & EXAMPLES
// ============================================================================

const calendar = new NativelyCalendar();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// calendar.retrieveCalendars(callback)
//   - Retrieves all calendars available on the device.
//
// calendar.createCalendarEvent(title, endDate, startDate, timezone, calendarId, description, callback)
//   - Creates a new event in the specified calendar.
//   - title: string
//   - endDate: Date — a JavaScript Date object, not a string
//   - startDate: Date — a JavaScript Date object, not a string
//   - timezone: string
//   - calendarId: string
//   - description: string

// ============================================================================
// CALLBACK RESPONSE FIELDS - retrieveCalendars
// ============================================================================
// resp.status: string — "SUCCESS" or "FAILED"
// resp.data: object — a map of calendar ID to calendar name,
//            e.g. { "1": "My calendar", "42": "email@address.com" }
// resp.error: string — error message, if any. Possible values:
//   - "no_available_calendars" — the user has no available calendars
//   - "cannot_retrieve_calendars" — something went wrong on the app side
//   - "calendar_permission_missing" — permission not granted

// ============================================================================
// CALLBACK RESPONSE FIELDS - createCalendarEvent
// ============================================================================
// resp.status: string — "SUCCESS" or "FAILED"
// resp.data.id: string — the calendar ID where the event was created
// resp.data.title: string — the event title
// resp.data.end: string — the event end date, in ISO 8601 format
// resp.data.start: string — the event start date, in ISO 8601 format
// resp.error: string — error message, if any. Possible values:
//   - "add_calendar_event_failure" — something went wrong on the app side
//   - "start_date_missing"
//   - "end_date_missing"
//   - "timezone_missing"
//   - "calendar_permission_missing" — permission not granted

// --- Calendars. Retrieve Calendars Example. Start ---

const retrieve_calendars_callback = function (resp) {
  if (resp.status === "FAILED") {
    console.log("Error:", resp.error); // e.g. "no_available_calendars", "calendar_permission_missing"
    return;
  }

  // resp.data is an object mapping calendar ID to calendar name
  // e.g. { "1": "My calendar", "42": "email@address.com" }
  for (const [calendarId, calendarName] of Object.entries(resp.data)) {
    console.log(calendarId, calendarName);
  }
};

calendar.retrieveCalendars(retrieve_calendars_callback);

// --- Calendars. Retrieve Calendars Example. End ---


// --- Calendars. Create Calendar Event Example. Start ---

const title = "Team Meeting";
const startDate = new Date("2025-07-10T14:00:00.000Z");
const endDate = new Date("2025-07-10T15:00:00.000Z");
const timezone = "Africa/Abidjan";
const calendarId = "calendar_id";
const description = "Event notes";

const create_calendar_event_callback = function (resp) {
  if (resp.status === "FAILED") {
    console.log("Error:", resp.error); // e.g. "start_date_missing", "timezone_missing"
    return;
  }

  console.log(resp.data.id);     // calendar ID
  console.log(resp.data.title);  // event title
  console.log(resp.data.end);    // event end date, ISO 8601
  console.log(resp.data.start);  // event start date, ISO 8601
};

calendar.createCalendarEvent(
  title,
  endDate,
  startDate,
  timezone,
  calendarId,
  description,
  create_calendar_event_callback
);

// --- Calendars. Create Calendar Event Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Calendars SDK: const calendar = new NativelyCalendar(); calendar.retrieveCalendars(callback) retrieves all calendars available on the device, returning resp.status ("SUCCESS" or "FAILED"), resp.data (an object mapping calendar ID to calendar name, e.g. { "1": "My calendar", "42": "email@address.com" }), and resp.error ("no_available_calendars", "cannot_retrieve_calendars", or "calendar_permission_missing" if status is "FAILED"). calendar.createCalendarEvent(title, endDate, startDate, timezone, calendarId, description, callback) creates a new event — endDate and startDate must be JavaScript Date objects, not strings — returning resp.status ("SUCCESS" or "FAILED"), resp.data.id (calendar ID), resp.data.title, resp.data.end, resp.data.start (ISO 8601 dates), and resp.error ("start_date_missing", "end_date_missing", "timezone_missing", or "calendar_permission_missing" if status is "FAILED"). For reference: https://docs.buildnatively.com/guides/integration/calendars
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Add a booking confirmation event to the user's calendar after checkout".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Check `resp.status` before using the result

Both `retrieveCalendars` and `createCalendarEvent` return a `status` field - always confirm it's `"SUCCESS"` before reading `resp.data`, and check `resp.error` when it's `"FAILED"` to handle the specific failure case.

#### Let the user pick a calendar before creating an event

Since a device may have multiple calendars, use `retrieveCalendars` first to show the user their available calendars (from the ID > name map), then pass the selected calendar's ID into `createCalendarEvent`.

#### Always construct real `Date` objects

`endDate` and `startDate` must be JavaScript `Date` instances, not date strings - pass a string and the method will throw. Use `new Date(...)` to construct them from your app's date data before calling `createCalendarEvent`.

#### Use this to confirm bookings or appointments

A common use case is adding a calendar event right after a booking or appointment is confirmed in your app, so the user doesn't have to manually add it themselves.

## Troubleshooting

<details>

<summary>Calendars permission denied</summary>

If the user previously denied calendar access, the feature can't prompt again automatically. Direct them to [Open App Settings](/guides/integration/open-app-settings) to manually re-enable the permission.

</details>

<details>

<summary><code>retrieveCalendars</code> returns <code>resp.error: "no_available_calendars"</code></summary>

The user's device has no calendars set up. There's nothing to select from until they add a calendar account on their device.

</details>

<details>

<summary>Not working in a web browser</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build or the Preview app.

</details>

[^1]: Replace this placeholder


# Camera

Let users capture photos or record videos directly within your app using the native camera.

## What is the Camera?

The Camera feature gives your app access to the device's native camera, letting users take photos or record videos without leaving the app. The captured media is returned as a Base64-encoded string, which you can display directly in your app or upload to your server.

Capturing a photo requests camera permission on both iOS and Android. Recording a video requests both camera and [microphone](/natively-platform/features/microphone) permissions on both platforms. Users will be prompted to grant these permissions the first time the feature is used.

## Prerequisites

{% hint style="success" %}
This feature requires any **paid** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Natively Dashboard Setup

{% hint style="success" %}
The Camera feature is automatically enabled for all new apps. You only need to visit this section if you want to disable it or update the Permission Description.
{% endhint %}

1. Open your Natively app dashboard and navigate to **Features > Camera**.
2. Toggle the feature to **Enabled**.
3. Enter the **Permission Description**.
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to users when the OS asks them to grant **camera** access. Explain clearly why your app needs this permission - for example: "We use your camera to let you upload photos."
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Camera

#### Events:

* **Camera Result Updated** - fires when the camera capture result is ready.
* **File Uploaded** - fires after the file was successfully uploaded to Amazon S3. The file URL is available in the element's state.
* **File Size over limit** - fires when the captured file exceeds the configured **File Size Limit**.

#### States:

* **Camera Result (Base64)** - Base64 string representation of the captured file. Can be used for custom uploading.
* **Camera Result (Content Type)** - `image/png`, `image/jpeg`, or `video/mp4`.
* **Uploaded File URL** - the Amazon S3 URL of the uploaded file.
* **File Size** - size of the latest camera result file, in KB.

#### Actions:

* **Show Camera**:
  * **Content Type** - `Photo` or `Video`.
  * **Quality** - `Low`, `Medium`, or `High`.
  * **Upload File** - checkbox. If checked, uploads the file directly to Bubble's Amazon S3.
  * **File Name** - used only if **Upload File** is checked.
  * **Camera Type** - `FRONT` / `BACK`.
  * **File Size Limit** - in **KB**. Prevents uploading files larger than this size to Bubble's Amazon S3.
    {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY CAMERA - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const camera = new NativelyCamera();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// camera.showCamera(type, quality, cameraType, callback)
//   - Opens the native camera.
//   - type: string — "photo" or "video"
//   - quality: string — "high" / "medium" / "low"
//   - cameraType: string — "FRONT" or "BACK"
//   - callback: function — called after the user captures media or cancels

// ============================================================================
// CALLBACK RESPONSE FIELDS
// ============================================================================
// resp.base64        - Base64 string of the captured media file
// resp.content_type  - "image/png" / "image/jpeg" / "video/mp4"
// resp.size          - File size in KB (as a string, e.g. "1024")

// --- Camera. Photo Example. Start ---

const open_camera_callback = function(resp) {
    console.log(resp.base64);
    console.log(resp.content_type);
    console.log(resp.size);
};

const type = "photo";
const quality = "high";
const cameraType = "BACK";

camera.showCamera(type, quality, cameraType, open_camera_callback);

// --- Camera. Photo Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Camera SDK: const camera = new NativelyCamera(); // camera.showCamera(type, quality, cameraType, callback) — opens the native camera. type: "photo"/"video". quality: "high"/"medium"/"low". cameraType: "FRONT"/"BACK". resp.base64: Base64 string of captured media. resp.content_type: "image/png"/"image/jpeg"/"video/mp4". resp.size: file size in KB (string). For reference: https://docs.buildnatively.com/natively-platform/features/camera
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Open the camera when the user taps the upload photo button, and display the captured image on the screen".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Choose the right type for your use case

Use `photo` for profile pictures, document scans, or single-image uploads. Use `video` when you need short clips - like a video review or a quick demo recording. Video capture requests both camera and microphone permissions.

#### Match quality to your use case

Use `high` for content that will be displayed prominently or needs to be sharp. Use `medium` or `low` for thumbnails or quick uploads where minimizing file size matters more than resolution.

#### Handle the Base64 result

The camera returns captured media as a Base64 string via `resp.base64`. Display it directly with a `data:` URI or upload it to your own server.

#### Check content\_type before processing

`resp.content_type` can be `image/png`, `image/jpeg`, or `video/mp4` depending on type and platform - always check this value rather than assuming a fixed format.

## Troubleshooting

<details>

<summary>Camera permission denied</summary>

If the user previously denied camera (or microphone, for video) access, the native camera prompt won't appear. Direct them to Open App Settings to manually re-enable the permission.

</details>

<details>

<summary>Camera feature not working after disabling and re-enabling</summary>

Since Camera can be toggled off in the dashboard, make sure you've re-enabled it, saved, and rebuilt the app - toggling the setting alone doesn't apply until a rebuild.

</details>

<details>

<summary>Not working in a web browser</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build or the Preview app.

</details>

[^1]: Replace this placeholder


# Contacts

Let users access and create entries in the device's native contacts, directly from your app.

## What is Contacts?

The Contacts feature gives your app access to the device's native contact list, letting users retrieve their existing contacts or create new ones without leaving the app. This is useful for features like inviting friends, syncing a personal address book, or letting users save a new contact your app provides.

Accessing contacts requires the user to grant permission the first time the feature is used, on both iOS and Android.

## Prerequisites

{% hint style="success" %}
This feature requires any **paid** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **Contacts**.
2. Toggle the feature to **Enabled**.
3. Enter the **Permission Description**.
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to users when the OS asks them to grant **contacts** access. Explain clearly why your app needs this permission — for example: "We use your contacts to help you invite friends to the app".
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Contacts

{% hint style="danger" %}
Set the element's data type field to **ContactObject (Natively - ...)**.
{% endhint %}

<figure><img src="/files/TlIxqpoH4UsDhNgIlRCG" alt=""><figcaption></figcaption></figure>

#### Events:

* **Get All Contacts Success** - fires when contacts are successfully retrieved.
* **Get All Contacts Failed** - fires if retrieving contacts fails.
* **Create Contact Success** - fires when a new contact is successfully created.
* **Create Contact Failed** - fires if creating a contact fails.

#### States:

* **Get Contacts Result** — list of `ContactObject`, each with:
  * **FirstName**;
  * **LastName**;
  * **Phones** — comma-separated if the contact has multiple phone numbers (e.g. `"0543523,124334234"`);
  * **Emails** — comma-separated if the contact has multiple emails (e.g. `"test@test.com,help@buildnatively.com"`);
  * **ID** — unique contact identifier on the device.
* **Create Contact ID Result** — the ID of the newly created contact, after **Create Contact**.
* **Create/Get Contacts Status** — result status after calling **Create Contact** or **Get All Contacts**.

#### Actions:

* **Get All Contacts** — returns a list of `ContactObject` .
* **Create Contact** — returns the new contact's ID:
  * **First name**;
  * **Last name**;
  * **Phone**;
  * **Email**;
    {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY CONTACTS - DOCUMENTATION & EXAMPLES
// ============================================================================

const contacts = new NativelyContacts();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// contacts.getAllContacts(callback)
//   - Retrieves all contacts stored on the device.
//
// contacts.createContact(firstName, lastName, email, phone, callback)
//   - Creates a new contact on the device.
//   - firstName: string
//   - lastName: string
//   - email: string
//   - phone: string

// ============================================================================
// CALLBACK RESPONSE FIELDS - getAllContacts
// ============================================================================
// resp.status: string — "SUCCESS" or "FAILED"
// resp.contacts: array — list of contact objects, each with:
//   id: string, firstName: string, lastName: string,
//   phones: array of strings, emails: array of strings

// ============================================================================
// CALLBACK RESPONSE FIELDS - createContact
// ============================================================================
// resp.status: string — "SUCCESS" or "FAILED"
// resp.id: string — the new contact's ID

// --- Contacts. Get All Contacts Example. Start ---

const get_all_contacts_callback = function (resp) {
  console.log(resp.status); // "SUCCESS" or "FAILED"

  resp.contacts.forEach(contact => {
    console.log(contact.id);         // unique contact identifier on the device
    console.log(contact.firstName);
    console.log(contact.lastName);
    console.log(contact.phones);     // array of phone numbers
    console.log(contact.emails);     // array of email addresses
  });
};

contacts.getAllContacts(get_all_contacts_callback);

// --- Contacts. Get All Contacts Example. End ---


// --- Contacts. Create Contact Example. Start ---

const firstName = "Jane";
const lastName = "Doe";
const email = "jane.doe@example.com";
const phone = "+1234567890";

const create_contact_callback = function (resp) {
  console.log(resp.status); // "SUCCESS" or "FAILED"
  console.log(resp.id);     // the new contact's ID
};

contacts.createContact(firstName, lastName, email, phone, create_contact_callback);

// --- Contacts. Create Contact Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Contacts SDK: const contacts = new NativelyContacts(); contacts.getAllContacts(callback) retrieves all contacts stored on the device, returning resp.status ("SUCCESS" or "FAILED") and resp.contacts (array of contact objects, each with id, firstName, lastName, phones (array), emails (array)). contacts.createContact(firstName, lastName, email, phone, callback) creates a new contact, returning resp.status ("SUCCESS" or "FAILED") and resp.id (the new contact's ID). For reference: https://docs.buildnatively.com/guides/integration/contacts
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Let the user create a new contact from a form with name, email, and phone fields".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Check `resp.status` before using the result

Both `getAllContacts` and `createContact` return a `status` field - always confirm it's `"SUCCESS"` before iterating over `resp.contacts` or using `resp.id`.

#### Handle multiple phone numbers and emails

A contact may have more than one entry in `phones` or `emails` - don't assume a single value; iterate over the array or pick the one relevant to your use case (e.g., the first entry).

#### Use this to power an invite-a-friend flow

A common use case is letting users pick from their existing contacts to invite friends to your app - retrieve contacts with `getAllContacts`, then let the user select recipients from the results.

## Troubleshooting

<details>

<summary>Contacts permission denied</summary>

If the user previously denied contacts access, the feature can't prompt again automatically. Direct them to [Open App Settings](/guides/integration/open-app-settings) to manually re-enable the permission.

</details>

<details>

<summary><code>getAllContacts</code> returns an empty list</summary>

This is expected if the user's device genuinely has no contacts.

</details>

<details>

<summary>Not working in a web browser</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build or the Preview app.

</details>

[^1]: Replace this placeholder


# Deep Links

Send users directly to specific pages or content inside your app from any link.

## What are Deep Links?

Deep Links let you link directly to specific content inside your app - just like linking to a page on a website. When a user taps a deep link, your app opens and navigates them to the right place automatically, whether that's a product page, a user profile, or a specific section of your app.

{% hint style="info" %}
Deep Links is the umbrella term used throughout this documentation. On iOS, this technology is officially called **Universal Links**. On Android, it is called **App Links**. Both work the same way from a user perspective.
{% endhint %}

Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](https://docs.buildnatively.com/~/changes/532/getting-started/subscription-plans)
{% endhint %}

## Supported methods

**Universal Links** - the recommended approach. Uses Apple's Universal Links on iOS and Android App Links on Android to associate your website domain directly with your app. No third-party service required.

{% content-ref url="/pages/6bA2cKs6reqqgSUk5Uwh" %}
[Universal Links](/natively-platform/features/deep-links/universal-links)
{% endcontent-ref %}

**Branch.io** - a third-party deep linking platform that adds advanced features like deferred deep linking, attribution, and analytics on top of standard deep links.

{% content-ref url="/pages/HPlQTQuXpjoMYjsDghaO" %}
[Branch.io](/natively-platform/features/deep-links/branch.io)
{% endcontent-ref %}


# Universal Links

Associate your website domain with your app so that links open directly in your app instead of a browser.

## What are Universal Links

Universal Links associate your website domain directly with your app - no third-party service required. When a user taps a link to your website, the device recognizes it as a deep link and opens your app instead of a browser, navigating them directly to the right page or content.

If the app is not installed on the device, the link opens normally in the browser, so your website always works as a fallback.

{% hint style="info" %}
Setups differ between iOS and Android. Use the sections below to follow the correct setup for your platform, or complete both if your app supports both platforms.
{% endhint %}

## iOS

### Prerequisites

* An [Apple Developer account](https://developer.apple.com/account).
* Your iOS app is already [published](/natively-platform/app-info/ios-build) in the Natively Dashboard.

### App Capabilities Configuration

{% hint style="info" %}
App capabilities define what system-level features your app is allowed to use on iOS. Before Natively can include Universal Links in your build, the **Associated Domains** capability must be explicitly enabled in your Apple Developer account for your app's Bundle ID.
{% endhint %}

1. Open your [Apple Developer account](https://developer.apple.com/account) and navigate to [Certificates, IDs & Profiles > Identifiers](https://developer.apple.com/account/resources/identifiers/list).
2. Select your app's Bundle ID.
3. Scroll down the **Capabilities** list and enable **Associated Domains**.
4. Click **Save** and confirm.

### apple-app-site-association

For Universal Links to work, your website must [host](#website-setup) a special verification file called the **Apple App Site Association (AASA)** file. This file tells iOS that your website and app are associated, allowing links to your domain to open in your app instead of a browser.

#### Download AASA template

{% file src="/files/nUsWhd1y36Vvx1yTDGXj" %}

{% hint style="danger" %}
The `webcredentials` section is required if you are using [Social Auth](/natively-platform/features/social-auth) in your app. Without it, OAuth authentication will not work correctly on iOS.
{% endhint %}

```json
{
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "TEAM_ID.BUNDLE_ID",
                "paths": [
                    "*"
                ]
            }
        ]
    },
    "webcredentials": {
        "apps": [
            "TEAM_ID.BUNDLE_ID"
        ]
    }
}
```

Replace `TEAM_ID` with your Apple Team ID and `BUNDLE_ID` with your iOS app's Bundle ID.&#x20;

{% hint style="info" %}
Your Apple **Team ID** is located in your [Apple Developer account](https://developer.apple.com/account) under **Membership details**.
{% endhint %}

<div><figure><img src="/files/txyHVYvLY3uNj8Wn9aMw" alt=""><figcaption><p>Team ID under Membership Detail</p></figcaption></figure> <figure><img src="/files/DAaG9cgA5fsXkuy34OP5" alt=""><figcaption><p>Bundle ID from App Store Connect iOS app</p></figcaption></figure></div>

{% hint style="warning" %}
On iOS 14 and later, Apple's CDN retrieves and caches the AASA file. When your app is installed, devices immediately download the file from the CDN. Devices check for updates approximately once per week after the app is installed. To download a newer version of the AASA file, reinstall the app. There is no direct CDN invalidation option.
{% endhint %}

{% hint style="warning" %}
It is up to third-party web browsers to enable Universal Links functionality. Universal Links may not be enabled in every web browser. Safari implements all of the functionality described in this page. For information about other browsers, check with the browser's vendor.
{% endhint %}

## Android

### Prerequisites

* Your Android app is already [published](/natively-platform/app-info/android-build) in the Natively Dashboard.
* Your app is [uploaded](https://support.google.com/googleplay/android-developer/answer/9845334#zippy=%2Cinternal-test-manage-up-to-testers) to the **Google Play Console** - required to access the **Upload key** SHA-256 certificate fingerprints.

### assetlinks.json

For Android App Links to work, your website must [host](#website-setup) a **Digital Asset Links** file. This file tells Android that your website and app are associated, allowing links to your domain to open in your app instead of a browser.

#### Download assetlinks.json template

{% file src="/files/XMFuKA52yvTJFcjuQ3ge" %}

```json
[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "BUNDLE_ID",
      "sha256_cert_fingerprints": [
        "APP_SIGNING_KEY_SHA256",
        "UPLOAD_KEY_SHA256"
      ]
    }
  }
]
```

Replace `BUNDLE_ID` with your app's Bundle ID. As for the SHA256 fingerprints:

* `APP_SIGNING_KEY_SHA256` — your **App signing key** SHA-256 certificate fingerprint.
* `UPLOAD_KEY_SHA256` — your  **Upload key** SHA-256 certificate fingerprint.

Both can be found in your **Google Play Console** app page under **Protected With Play** > **Play Store protection** > **Protect app signing key** > **Manage Play App Signing**.

{% hint style="info" %}
If you don't see the **Upload key certificate** content, you need to [upload](https://support.google.com/googleplay/android-developer/answer/9845334#zippy=%2Cinternal-test-manage-up-to-testers) your app first.
{% endhint %}

<div><figure><img src="/files/ReoNA0pNzW0p3SHHztJc" alt=""><figcaption><p>Google Play Console Manage Play app signing</p></figcaption></figure> <figure><img src="/files/s71lOvMgwc4u8kVnPOfU" alt=""><figcaption><p>SHA-256 fingerprints</p></figcaption></figure></div>

## Website setup

Before proceeding, make sure you have already created and configured your verification files for [iOS](#apple-app-site-association) and [Android](#assetlinks.json).

Once your files are ready, follow the instructions below for your platform to host them on your website.

{% tabs %}
{% tab title="General" %}
To make Universal Links work, your website needs to host two special files at specific locations that Apple and Google can access to verify your domain.

Upload the relevant file(s) for your platform::

* `apple-app-site-association` - required for iOS, must be accessible at <mark style="color:red;">`https://yourdomain.com/.well-known/apple-app-site-association`</mark>
* `assetlinks.json` - required for Android, must be accessible at <mark style="color:red;">`https://yourdomain.com/.well-known/assetlinks.json`</mark>&#x20;

Files must be publicly accessible without any redirects.

{% hint style="warning" %}
Files must be served with the content type `application/json`. If the content type is incorrect, Apple and Google might not be able to verify your domain even if the files are accessible.
{% endhint %}

If you are unsure how to host files on your website, check the other tabs for platform-specific instructions.
{% endtab %}

{% tab title="Bubble.io" %}

1. Open your Bubble project.
2. Go to the **Settings > SEO / metatags**.
3. Scroll down and find the **Hosting files in the root directory** section.
4. For iOS: set the name to `.well-known/apple-app-site-association` and upload your **AASA** file.
5. For Android: set the name to `.well-known/assetlinks.json` and upload your **assetlinks.json** file.
6. Publish the changes.

![](/files/XgQ1iwyrnipw2XxtbzpR)
{% endtab %}

{% tab title="AI Agents" %}
Lovable, Replit, and Base44 all use React/Vite under the hood, which copies everything in the `public/` folder to the build output root as-is. This means you can host the verification files simply by asking your AI assistant to create them at the correct paths.

{% hint style="warning" %}
Copy your configured file content from the [iOS](#apple-app-site-association) or [Android](#assetlinks.json) sections above, paste it into the placeholder at the end of the copied text, then send it to your AI agent.
{% endhint %}

For the iOS app, copy the line below and paste it into your AI agent:

```
Create a file at public/.well-known/apple-app-site-association with the following content: [paste your configured apple-app-site-association content here]
```

For the Android app, copy the line below and paste it into your AI agent:

```
Create a file at public/.well-known/assetlinks.json with the following content: [paste your configured assetlinks.json content here]
```

Once published, the files will be automatically accessible on your domain.
{% endtab %}
{% endtabs %}

## How to verify?

Once your verification files are hosted, confirm everything is configured correctly before proceeding to the [Natively Dashboard Setup](#natively-dashboard-setup).

{% hint style="info" %}
Both tools check the live hosted files, not your local copies. Make sure you have published your changes before running the verification.
{% endhint %}

### iOS

{% embed url="<https://branch.io/resources/aasa-validator/>" %}

Use the Branch AASA Validator to verify your `apple-app-site-association` file. Enter the following details to confirm the file is accessible, correctly served, and valid:

* **Domain** - your website domain.

### Android

{% embed url="<https://developers.google.com/digital-asset-links/tools/generator>" %}

Use the Google Digital Asset Links tool to verify your `assetlinks.json` file. Enter the following details to confirm the file is correctly hosted and your app is properly associated with your domain:

* **Hosting site domain** - your website domain.
* **App package name** - your app's Bundle ID.
* **App package fingerprint (SHA256)** - either your app signing key or upload key SHA256 fingerprint from the Google Play Console.

#### Checking via Android device settings

You can also verify directly on your Android device. Go to **Settings > Apps > \[your app] > Open by default** (also called **Supported web addresses** on some devices):

* If the toggles are **enabled** or the link is present but there are **no toggles** (behavior depends on Android vendor) - Android Universal Links are correctly configured.
* If the toggles are **present but disabled** - the `assetlinks.json` is incorrect or couldn't be verified by Android. The app is configured for deep links, but Android couldn't confirm domain ownership.

## Natively Dashboard Setup

{% hint style="info" %}
Before proceeding, make sure your verification files are hosted and accessible. You can confirm this using the tools in the [How to verify](#how-to-verify) section above. If there are any issues with your files, Natively Dashboard will indicate a configuration problem, and the feature cannot be enabled until they are resolved.
{% endhint %}

1. Open your Natively app dashboard and navigate to **Features** > **Deep Links** > **Universal Links**.
2. Toggle the feature to **Enabled**.
3. Enter your **Associated Domain** - your website domain (e.g. `yourdomain.com`).
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

{% hint style="info" %}
Natively will prefill the **Associated Domain** with your app's URL configured in the Natively Dashboard. You can modify it if your deep links domain differs from your app's main URL.
{% endhint %}

## Troubleshooting

<details>

<summary>Website Links are opening in a browser instead of the app</summary>

Most commonly caused by one of the following:

* Verification files are not correctly hosted or formatted.
* The Associated Domain in the Natively Dashboard doesn't match the domain where your files are hosted.
* App not rebuilt after enabling the feature.

</details>

<details>

<summary>Universal Links are not working after updating the AASA file</summary>

Apple's CDN caches the AASA file and checks for updates approximately once per week. To force a refresh, reinstall the app on your device.

</details>

<details>

<summary>Universal Links are not working in third-party browsers</summary>

Third-party browsers may not support Deep Links on either platform. On iOS, Universal Links are only guaranteed to work in Safari. On Android, behavior varies by browser - some browsers, like Samsung Internet, may not open the app correctly, while others, like Firefox on the same device, may work fine. If Universal Links are not working in a specific browser, check with the browser's vendor for more information.

</details>

<details>

<summary>Android app not opening from an Associated Domain link</summary>

Check the following:

* The file is served with the content type `application/json` .
* The file is accessible without any redirects.
* Both SHA-256 fingerprints (app signing key and upload key) are present in the file; if either one is missing, the app will not open.
* The package name matches your app's Bundle ID exactly.

</details>

<details>

<summary>iOS app not opening from an Associated Domain link</summary>

Check the following:

* The file is served with the content type `application/json` .
* The file is accessible without any redirects.
* Both `applinks` and `webcredentials` sections are present and correctly formatted.
* The Team ID and Bundle ID are correct.

</details>


# Branch.io

Set up Branch.io deep linking in your Natively app for reliable cross-platform deep links with attribution and analytics.

## What is Branch.io

Branch.io is a third-party deep linking platform that guarantees reliable navigation to your app, even if the app is not installed, by directing users to the appropriate app store. On top of standard deep linking, Branch provides link performance tracking, user journey optimization, and marketing attribution, making it a good choice for apps with active marketing campaigns.

{% hint style="info" %}
If you only need basic deep linking without attribution or analytics, consider using [Universal Links](/natively-platform/features/deep-links/universal-links) instead, since they require no third-party service.
{% endhint %}

## Prerequisites

* A [Branch.io](https://www.branch.io/) account.
* Your [iOS](/natively-platform/app-info/ios-build) and/or [Android](/natively-platform/app-info/android-build) app fully configured in the Natively Dashboard.

## Branch.io setup

Once your Branch.io account is created, you'll be directed to configure your app's default link behaviors. Follow the steps below to complete the setup.

### Default URL

1. Go to **App Settings** in the sidebar.
2. **Default URL:** Insert your app's URL.

### Configure Android redirects

1. Check **I have an Android App**.
2. Enter your **Android URI Scheme** - your app's **Bundle ID** in lowercase, followed by `://`. For example: `com.example.natively://`.
3. Select your app's store listing:
   * If your app is already published: select **Google Play Search** and find your app.
   * If your app is not yet published: select **Custom URL**, then enter your app's **URL** and **Bundle ID**.
4. Check the box **Enable App Links** and enter your **SHA256 certificate fingerprints**, as a comma-separated list.

{% hint style="info" %}
Your SHA256 certificate fingerprints can be found in Google Play Console under **Protected With Play** > **Play Store protection** > **Protect app signing key** > **Manage Play App Signing**.
{% endhint %}

{% hint style="info" %}
If you don't see the **Upload key certificate** content, you need to [upload](https://support.google.com/googleplay/android-developer/answer/9845334#zippy=%2Cinternal-test-manage-up-to-testers) your app first.
{% endhint %}

<figure><img src="/files/GynFnCX4eGJvkuyKnkaX" alt=""><figcaption><p>Android Redirects setup</p></figcaption></figure>

### Configure iOS redirects

1. Check **I have an iOS App**.
2. Enter your **iOS URI Scheme** - your app's **Bundle ID** in lowercase, followed by `://`. For example: `com.example.natively://` .
3. Select your app's store listing:
   * If your app is already published: select **Apple Store Search** and find your app.
   * If your app is not yet published: select **Custom URL**, then enter your app's **URL** and App Store **App ID**.
4. Check **Enable Universal Links** and enter the following:
   * **Bundle Identifier** - your app's Bundle ID.
   * **Apple App Prefix** - open your [Apple Developer account > Identifiers](https://developer.apple.com/account/resources/identifiers/list), select your app's **Bundle ID**, and copy the **App ID Prefix** shown at the top of the identifier details page.
5. Check **Enable NativeLink** and set **Audience Rule** to **All iOS traffic**.

<figure><img src="/files/Go6aosyT2KBIhQZlpUNG" alt=""><figcaption><p>iOS Redirects setup</p></figcaption></figure>

### Link Domain

1. Set up a subdomain or your own custom domain.
2. Enter your **Default Link Domain** (e.g. `yourdomain.app.link`).
3. Enter your **Alternate Link Domain** (e.g. `yourdomain-alternate.app.link`).
4. Click **Save**.

<figure><img src="/files/9rhfqke2jfaz8PA6pq7E" alt=""><figcaption><p>Link Domain setup</p></figcaption></figure>

## Natively Dashboard setup

1. Go to [Branch.io Account Settings](https://dashboard.branch.io/account-settings/profile) and copy the **Branch Key**.
2. Open your Natively app's dashboard and navigate to **Features** > **Deep Links** > **Branch**.
3. Toggle the feature to **Enabled**.
4. Enter the **Default Link Domain** from the [Link Domain](#link-domain) step above in the **Domain** field.
5. Enter the **Alternate Link Domain** from the [Link Domain](#link-domain) step above in the **Alt Domain** field.
6. Enter the **Branch Key** in the **Key** field.
7. Click **Save**.
8. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

<div><figure><img src="/files/F7N2zy2lTs5vWJpICdOs" alt=""><figcaption><p>Branch Key from Branch.io Account Settings</p></figcaption></figure> <figure><img src="/files/I63TwZKIAXKDectJDfZu" alt=""><figcaption><p>Branch.io Deep Links Natively Dashboard setup</p></figcaption></figure></div>

## How to use

Once Branch.io is configured and your app is rebuilt, you can start creating deep links. A Branch deep link follows this format:

`{default_link_domain}/?$deeplink_path={url}`

**Example:**

`https://yourdomain.app.link/?$deeplink_path=https://yourdomain.io/test/reset_pw?test`

To create and manage your deep links, use the [Branch.io Dashboard](https://dashboard.branch.io).

## Troubleshooting

<details>

<summary>Deep links are not opening the app</summary>

Make sure the URI scheme, Bundle ID, and SHA256 fingerprints are correctly configured in the Branch.io dashboard. Verify that your app has been rebuilt after saving the Natively Dashboard configuration.

</details>

<details>

<summary>Google Play Data Safety section rejection</summary>

Developers are required to fill out Google's updated Data Safety section in the Google Play Console when using Branch.io. Without approval in the Data Safety section, your new app submission or app update may be rejected.

For detailed information on completing the Google Play Store questions: [Answering the Google Play Store Privacy Questions](https://help.branch.io/using-branch/docs/answering-the-google-play-store-privacy-questions).

</details>

<details>

<summary>Branch Key not working</summary>

Make sure you are using the correct Branch Key from your [Branch.io Account Settings](https://dashboard.branch.io/account-settings/profile). Verify that the Default and Alternate Link Domains match exactly what is configured in Branch.io.

</details>

<details>

<summary>Universal Links not working on iOS</summary>

Make sure **Enable Universal Links** and **Enable NativeLink** are both checked in the Branch.io dashboard. Verify that the Bundle Identifier and Apple App Prefix are correctly entered.

</details>


# Geolocation

<figure><img src="/files/FsCxzAYrasuVowPnvTdj" alt=""><figcaption></figcaption></figure>

### How to set up geolocation?

Turn on the **Location** feature:

* **Permission description** - The permission description text should explain to the user why your app needs that permission. Refer to [**Apple's guidelines** ](https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/accessing-user-data/)and [Google's guidelines](https://support.google.com/googleplay/android-developer/answer/9799150?hl=en) to avoid potential **rejection**.

### How to set up background location tracking?

{% hint style="danger" %}
To start using background location tracking. You need to enable a Geolocation and fill out the Permission description for iOS (If you're planning to use iOS).
{% endhint %}

* **Permission description** - The permission description text should explain to the user why your app needs that permission. Refer to [**Apple's guidelines** ](https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/accessing-user-data/)and [Google's guidelines](https://support.google.com/googleplay/android-developer/answer/9799150?hl=en) to avoid potential **rejection**.

{% hint style="info" %}
To make background location tracking work correctly, you need to ask the user to give Always allow permission. The system will handle that for you, but make sure you specify it in the permission description for Android.\
For iOS OS will ask users to allow background tracking automatically once they collapse that app.
{% endhint %}

* **Webhook URL** - This is your endpoint URL where we will send a user's geolocation. A few requirements for an endpoint:
  * HTTPS (HTTP is not supported)
  * POST method
  * JSON in a body

```json
// JSON of what you should expect to be received from a device 
// Example
{
	"latitude": 50.000001,
	"longitude": 50.000001,
	// Identifier that will be passed later (Check Integration for more details)
	"identifier": "TEST", 
	// Error can be null or string (which will contain an error message)
	"error": null // "Data Discarded", "Timeout", "Authorization Needed", "Internal Server Error", "Parsing Error", "User Cancelled", "Invalid/Missing API Key", "Quota limit reached", "Not Found", "Reserved IP", "Not Supported", "Invalid polygon. Must be 1 or more points", "Network error: \(statusCode)"
}
```

* **Header Name & Value (Optional)** - In case you have authorization on your webhook endpoint. This header will be sent to your endpoint from a device.

### How to create a webhook with Bubble?

* [https://manual.bubble.io/core-resources/api/workflow-api](https://manual.bubble.io/help-guides/apis-connect-to-other-apps/the-bubble-api/the-workflow-api/workflow-api-endpoints)
* [How to setup Bubble's Workflow API Endpoint for your Application](https://youtu.be/6MxCxUroY4s)

### How to use geolocation?

{% content-ref url="/pages/Y8G0HqQlG1nxXNd0quly" %}
[Geolocation](/guides/integration/geolocation)
{% endcontent-ref %}

### I'm already utilizing geolocation permissions on my website. Is it necessary to implement a native geolocation API in my app?

Yes, we advise using the native geolocation API for an enhanced user experience, but the web geolocation API is also an option, albeit with repeated permission prompts each time the app is accessed.


# HealthKit

Access and share health and fitness data while maintaining the user’s privacy and control.

{% hint style="danger" %}
HealthKit is an Apple Framework that is available only on iPhones. No iPads or Android devices.
{% endhint %}

### How to set up HealthKit?

1. **Enable HealthKit for your Bundle ID on** [**https://developer.apple.com**](https://developer.apple.com)

   \
   a) Go to [Bundle IDs page](https://developer.apple.com/account/resources/identifiers/bundleId)

   <figure><img src="/files/A2IjvvtzDMaSCtZn5vm6" alt=""><figcaption></figcaption></figure>

   \
   b) Find your Bundle ID in a list and click on it

   <figure><img src="/files/ELMIo6DKmBIljB3NmU8N" alt=""><figcaption></figcaption></figure>

   \
   c) Scroll down, enable HealthKit, and click Save

   <figure><img src="/files/Vr7p92Si65vhphfzrVnY" alt=""><figcaption></figcaption></figure>

   <br>
2. **Enable HealthKit in Natively**

   <figure><img src="/files/yqVVvBXNH3FK690Ob2Qp" alt=""><figcaption></figcaption></figure>

   Turn on the HealthKit feature and fill out the following information:

   * **Read permission description** - A message to the user that explains why the app requested permission to read samples from the HealthKit store. Refer to [**Apple's guidelines** ](https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/accessing-user-data/)to avoid potential **rejection**.
   * **Write permission description** - A message to the user that explains why the app requested permission to save samples to the HealthKit store. Refer to [**Apple's guidelines** ](https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/accessing-user-data/)to avoid potential **rejection**.

{% hint style="warning" %}
For now, Natively **supports only reading data** from HealthKit (We plan to add writing soon). But **Apple requires** apps that only read data to provide **both Read/Write permission text**. You can **use Read text for both Read & Write**.
{% endhint %}

3. **Go to Natively and rebuild your app**

### How to use HealthKit?

{% content-ref url="/pages/aADCd43iTMPbOaNvpdLE" %}
[HealthKit](/guides/integration/healthkit)
{% endcontent-ref %}


# iOS Back Button

Add an on-screen back button for iOS, since iOS devices have no native back button.

## What is iOS Back Button?

The iOS Back Button is an on-screen navigation control placed in the bottom-left corner of the screen. Tapping it returns the user to the previous page in their browsing history - replicating the behavior of Android's native back button, which iOS doesn't have. This is especially useful if your website relies on browser history or URL parameters for navigation.

{% hint style="warning" %}
This feature relies on your website's URL-based navigation history. If your website uses a navigation method that doesn't update the URL - such as single-page app routing without history updates - there may be no history for the button to go back to.
{% endhint %}

### Visual examples

<div><figure><img src="/files/QU54wQ2qBI60XWSlg5bk" alt=""><figcaption><p>Light Theme</p></figcaption></figure> <figure><img src="/files/XBNPQINq3VVPQxL6KQRI" alt=""><figcaption><p>Light Theme (with Bottom Bar enabled)</p></figcaption></figure> <figure><img src="/files/BeYX1EGjBV2H2xC5lu8O" alt=""><figcaption><p>Dark Theme</p></figcaption></figure> <figure><img src="/files/T6elj7wIhX19gynlzsd5" alt=""><figcaption><p>Dark Theme (with Bottom Bar enabled)</p></figcaption></figure></div>

## Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **iOS Back Button**.
2. Toggle the feature to **Enabled**.
3. Select a **Theme** - **Light** or **Dark**.
4. Click **Save**.
5. Rebuild your iOS app.

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Troubleshooting

<details>

<summary>The button doesn't appear after enabling it</summary>

Confirm the app has been rebuilt after toggling the feature on - like all Dashboard-configured features, this requires a rebuild to take effect.

</details>

<details>

<summary>Tapping the button exits the app instead of going back</summary>

This means there's no navigation history to go back to - most commonly because your website's navigation doesn't update the URL when moving between pages or sections (e.g., single-page app routing without history updates).

</details>

<details>

<summary>Not working in the Natively Preview app</summary>

Appearance and navigation-related settings like this one are not guaranteed to be reflected in the Preview app. Test on a real device using a full build.

</details>


# In-App Purchases

Sell digital subscriptions and one-time products inside your app using Google's and Apple's native purchase systems.

## What are In-App Purchases?

Natively provides built-in support for [RevenueCat](https://www.revenuecat.com/?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) to handle in-app purchases and subscriptions across iOS and Android. RevenueCat manages purchase validation, receipt handling, and subscription status for you, so you don't have to build and maintain this infrastructure yourself.

{% hint style="danger" %}
Apple and Google require apps to use their native in-app purchase systems - not external payment processors like Stripe - for digital goods, content, and subscriptions sold within the app. Physical goods and real-world services are the main exception. See [How to use](#know-when-stripe-is-and-isnt-allowed) for more information.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* A [RevenueCat](https://www.revenuecat.com/?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) account.
* **iOS:**
  * An [Apple Developer](https://developer.apple.com/account) account.
  * Your iOS app is already [published](/natively-platform/app-info/ios-build) in the Natively Dashboard.
* **Android:**
  * A [Google Play Developer](https://play.google.com/console) account.
  * Your Android app is already [published](/natively-platform/app-info/android-build) in the Natively Dashboard.

## App Stores Product Configuration

Before RevenueCat can validate purchases, your subscription or one-time-purchase products must exist in Google Play Console (for Android) and/or App Store Connect (for iOS).

{% tabs %}
{% tab title="Google Play Console" %}
{% hint style="danger" %}
Your Google Play Console account must have a **Google Merchant account** configured before you can sell in-app products or subscriptions. If you haven't set this up yet, Google Play will prompt you to configure it when you first try to create a product.
{% endhint %}

{% hint style="info" %}
If you don't see an option to create subscriptions or in-app products yet, upload a build first - create a release in a **Closed testing** track and upload Android **.AAB** file from Natively Dashboard.
{% endhint %}

1. Open your app in [Google Play Console](https://play.google.com/console) and go to **Monetize with Play** > **Products**.
2. Follow [RevenueCat's Android Product Setup](https://www.revenuecat.com/docs/getting-started/entitlements/android-products?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide to create your subscription or in-app products.
3. Confirm your subscription products show as **Active** in Google Play Console.
   {% endtab %}

{% tab title="App Store Connect" %}
{% hint style="danger" %}
Your **Paid Applications Agreement** must be signed before you can set up or test any in-app purchases. In **App Store Connect**, go to the **Business** module and sign the latest version of the agreement. You'll also need to complete the **Tax** and **Banking** tabs, with a linked bank account showing a **Clear** status - in-app purchases won't work in sandbox or production until all of this is complete.
{% endhint %}

1. Open [App Store Connect](https://appstoreconnect.apple.com/) and navigate to your app.
2. Follow [RevenueCat's iOS Product Setup](https://www.revenuecat.com/docs/getting-started/entitlements/ios-products?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide to create your subscription or one-time purchase products.
3. Confirm each product's state shows **Ready to Submit** before it can be used - even for sandbox testing.
   {% endtab %}
   {% endtabs %}

## RevenueCat Configuration

With your store products created, connect RevenueCat to each store so it can manage the full purchase lifecycle on your behalf.

{% stepper %}
{% step %}

#### Create your RevenueCat project

1. Sign up or log in at [RevenueCat](https://www.revenuecat.com/?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner).
2. Click **New Project**, give it a name - typically your app's name - and confirm.

Each RevenueCat project can hold one or more apps (e.g., your iOS and Android apps), and is where all your Products, Entitlements, Offerings, and API Keys will live.
{% endstep %}

{% step %}

#### Add and connect your app(s)

1. In your RevenueCat project, go to **Apps & providers** in the left sidebar.
2. Click **New app configuration**.
3. Select **Google Play Store** for Android, and **App Store** for iOS.
4. Configure the platforms:

{% tabs %}
{% tab title="Google Play Store" %}

1. Enter the Android app **Package Name** (Bundle ID) configured in your Natively Dashboard.
2. Upload your **Service Account Credentials JSON**. Follow [RevenueCat's Google Play Store service credentials](https://www.revenuecat.com/docs/service-credentials/creating-play-service-credentials?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide for the full walkthrough.
3. Click **Save changes**.
   {% endtab %}

{% tab title="App Store" %}

1. Enter the iOS app **Bundle ID** configured in your Natively Dashboard.
2. Upload your **In-app Purchase Key**. Follow [RevenueCat's In-App Purchase Key](https://www.revenuecat.com/docs/service-credentials/itunesconnect-app-specific-shared-secret/in-app-purchase-key-configuration?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide for the full walkthrough.
3. Add your **App Store Connect API** key. Follow [RevenueCat's App Store Connect API Key](https://www.revenuecat.com/docs/service-credentials/itunesconnect-app-specific-shared-secret/app-store-connect-api-key-configuration?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide for the full walkthrough.
4. Click **Save changes**.

{% hint style="info" %}
The **App Store Connect API** key you already created for publishing your iOS app in the Natively Dashboard can be reused here - no need to generate a new one.
{% endhint %}
{% endtab %}
{% endtabs %}
{% endstep %}

{% step %}

#### Set up your Product Catalog

Once your app is connected to each store, bring your products into RevenueCat and organize them.

{% hint style="warning" %}
You must have at least one Entitlement and at least one Offering to use RevenueCat with Natively.
{% endhint %}

{% stepper %}
{% step %}

#### Import your product(s)

1. In RevenueCat, go to **Product Catalog** in the left sidebar, then click **Products**.
2. Find your connected store (App Store or Play Store) and click **+ New**.
3. Click **Import Products** and select the products you created earlier in App Store Connect and/or Google Play Console.
4. Click **Import**.

Repeat for each platform you support - iOS and Android products need to be imported separately, even if they represent the same offer.

Check [RevenueCat's Import Products](https://www.revenuecat.com/docs/offerings/products-overview#import-products-to-revenuecat?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide for the full walkthrough.
{% endstep %}

{% step %}

#### Create Entitlement(s)

Entitlements represent the access level a purchase unlocks in your app - for example `premium`, `pro`, or `gold`. Most apps need only one entitlement; create more only if you offer distinct tiers.

1. Go to **Product Catalog** > **Entitlements** and click **+ New Entitlement**.
2. Give it an identifier (e.g. `premium`) and the display name, then click **Add**.
3. Open the entitlement you just created. In the **Associated products** section, click **Attach** and select the products that should unlock it, including the equivalent product from each platform.

Check [RevenueCat's Entitlements](https://www.revenuecat.com/docs/getting-started/entitlements?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide for the full walkthrough.

{% hint style="info" %}
Avoid spaces in the identifier - use something like `premium` or `pro_tier` instead of `Premium Access`. The identifier is what your app's code checks against, so keeping it simple and consistent avoids issues later.
{% endhint %}
{% endstep %}

{% step %}

#### Create Offering(s)

Offerings control what your app actually displays to users, and let you change pricing, packaging, or messaging remotely - without shipping an app update.

1. Go to **Product Catalog > Offerings**.&#x20;
2. RevenueCat automatically creates a **default** offering for you - you can use it, or click **+ New** to create an additional one if you need multiple offerings (e.g., for different user segments).
3. Open the offering and click **Edit** to add/change the **Packages** - these group equivalent products across platforms (e.g., a monthly subscription available on both iOS and Android) under a single identifier your app's code can use.

Check [RevenueCat's Offerings](https://www.revenuecat.com/docs/offerings/overview?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide for the full walkthrough.

{% hint style="info" %}
RevenueCat provides predefined package identifiers for common cases (e.g. `$rc_monthly`, `$rc_annual`), or you can create your own custom identifier if none of the predefined ones fit.&#x20;
{% endhint %}

{% hint style="warning" %}
Package identifiers must be distinct across your offerings - reusing the same identifier in more than one offering can cause unpredictable behavior.
{% endhint %}
{% endstep %}
{% endstepper %}
{% endstep %}

{% step %}

#### Retrieve your Public API Keys

1. In RevenueCat, go to **API Keys** in the left sidebar.
2. Under **SDK API Keys**, copy the **Public API key** for each platform you support.
   {% endstep %}
   {% endstepper %}

## Natively Dashboard Setup

Before proceeding, make sure you have completed the [RevenueCat Configuration](#revenuecat-configuration) steps above.

1. Open your Natively app dashboard and navigate to **Features** > **In-App Purchases**.
2. Toggle the feature to **Enabled**.
3. Paste the **Public API Keys** you copied from RevenueCat into the respective fields:
   * **iOS API Key** - starts with `appl_` .
   * **Android API Key** - starts with `goog_`.
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="warning" %}
Make sure you're using your **App Store** (`appl_...`) or **Play Store** (`goog_...`) key - not RevenueCat's **Test Store** key. The Test Store is a separate sandbox environment inside RevenueCat and won't work with purchases in your app.
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Purchases

#### Events:

* **Set Customer Success** - fires when **Set Customer ID** completes successfully.
* **Set Customer Failed** - fires when **Set Customer ID** fails.
* **Customer ID Received** - fires when **Get Customer ID** returns a result.
* **Get Price Success** - fires when **Get Package Price** completes successfully.
* **Get Price Failed** - fires when **Get Package Price** fails.
* **Purchase Success** - fires when a purchase completes successfully.
* **Purchase Failed** - fires when a purchase fails due to an error.
* **Purchase Cancelled** - fires when the user backs out of the purchase flow. Handle this separately from **Purchase Failed** - it isn't an error.
* **Restore Purchase Success \[iOS]** - fires when **Restore Purchase** successfully finds and restores prior purchases.
* **Restore Purchase Failed \[iOS]** - fires when **Restore Purchase** fails.
* **Paywall - purchase success** - fires when the user completes a purchase through the paywall.
* **Paywall - cancelled** - fires when the paywall is shown, and the user closes it without taking action.
* **Paywall - error** - fires when the paywall was shown and an error occurred during an operation.
* **Paywall - was not presented** - fires when the paywall doesn't display at all.
* **Paywall - Entitlement ID missing** - fires only when using **Show Paywall if needed** without providing an Entitlement ID.
* **Paywall - Offering ID was not found** - fires only when the provided Offering ID is invalid.
* **Paywall - restore purchase success \[iOS]** - fires when the user restores a purchase from within the paywall.

#### States:

* **Latest Transaction Id** - the transaction ID after **Purchase Package**.
* **Latest Customer Id** - the customer ID after **Set Customer ID** or **Purchase Package**.
* **Latest Error** - empty if no error occurred.
* **Latest GetPrice price** - the raw numeric price, e.g. `9.99`.
* **Latest GetPrice price (Localized)** - the localized price string, e.g. `"$9.99"` or `"9.99 USD"`.
* **Latest GetPrice currency** - the currency code, e.g. `"USD"`, `"UAH"`.
* **Latest PurchasePackage packageId** - the package ID from the most recent purchase attempt.

#### Actions:

* **Purchase Package** - initiates the purchase of a product or subscription:
  * **Package ID** - the package to purchase.
  * **Old Product ID (Android)** - required only when upgrading or downgrading an active Android subscription. This must be the real store Product ID, not a package ID - retrieve it via **RevenueCat - Verify Subscription** below.
  * **Proration (Android)** - controls exactly when the user is charged and how their billing cycle adjusts during an upgrade or downgrade. See [RevenueCat's Proration](https://www.revenuecat.com/blog/engineering/google-proration?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) documentation for a full breakdown.
* **Set Customer ID** - links your user's ID to a RevenueCat customer:
  * **Customer Identifier** - recommended to use your Current User's unique ID, so purchases are tied to the same account across devices.
* **Reset Customer ID** - unlinks the current Customer ID from this device.
* **Get Package Price** - retrieves the price for a specific package.
* **Get Customer ID** - retrieves the current customer ID.
* **Show Paywall** - displays a RevenueCat paywall:
  * **Offering ID** - optional; shows this specific offering instead of the default.
* **Show Paywall if needed** - displays a paywall only if the user doesn't already have the specified entitlement:
  * **Offering ID** - optional; shows this specific offering instead of the default.
  * **Entitlement ID** - required.
* **Restore Purchase \[iOS]** - restores previous purchases tied to the user's App Store account. RevenueCat handles the underlying restore logic automatically.

#### RevenueCat - Verify Subscription

{% hint style="info" %}
Unlike the actions above, this checks entitlement status directly against RevenueCat's servers rather than through the Natively SDK - useful for confirming a user's real subscription state, or for retrieving the store Product ID needed for Android upgrades/downgrades.
{% endhint %}

{% hint style="info" %}
This action requires your RevenueCat **Secret API Key** - a different key from the Public API Keys used in Natively Dashboard Setup. Add it as `revenuecat_apiKey` in the Bubble plugin settings. Find it under **RevenueCat App** > **API keys** > **Secret API keys**, and confirm you're using an **API Key V1** - other versions won't work.
{% endhint %}

* **Customer ID** - the RevenueCat user ID used during **Set Customer ID**. If no customer exists with this ID, RevenueCat creates a new one.
* **Entitlements ID** - the entitlement identifier to check.

#### Returns:

* **Product ID** - the store product ID tied to the active entitlement.
* **Purchase Date**
* **Grace Period Expires Date**
* **Expiration Date**
* **Is Active** - Yes/No. Yes - when **Expiration Date** is after the current date.
* **Error** - empty text if no error occurred.
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY IN-APP PURCHASES - DOCUMENTATION & EXAMPLES
// ============================================================================

const purchases = new NativelyPurchases();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// purchases.login(userId, userEmail, callback)
//   - Links your app user to a RevenueCat customer.
//   - userId: string — a stable internal ID, recommended to use the same
//     value across web and mobile for the same user.
//   - userEmail: string — optional.
//
// purchases.logout(callback)
//   - Unlinks the current user, reverting to an anonymous RevenueCat ID.
//
// purchases.customerId(callback)
//   - Retrieves the current customer ID. Useful when you're not explicitly
//     linking the user via login().
//
// purchases.restore(callback)
//   - Restores previous purchases tied to the user's App Store / Google Play
//     account. Especially important on iOS.
//
// purchases.purchasePackage(packageId, callback, oldProductId, prorationMode)
//   - Purchases a specific package.
//   - packageId: string — e.g. "$rc_monthly".
//   - oldProductId: string — optional. Android only. Required when upgrading or
//     downgrading an active subscription. This must be the real store
//     Product ID (e.g. "com.company.app.premium:monthly"), not a package ID
//     like "$rc_monthly" — retrieve it from the platformProductId field
//     returned by getOfferings().
//   - prorationMode: string — optional. Android only. Controls when the user is
//     charged and how their billing cycle adjusts. Supported values:
//       "immediateWithoutProration"       — (WITHOUT_PRORATION);
//       "immediateWithTimeProration"      — (WITH_TIME_PRORATION);
//       "immediateAndChargeFullPrice"     — (CHARGE_FULL_PRICE);
//       "immediateAndChargeProratedPrice" — (CHARGE_PRORATED_PRICE);
//       "deferred"                        — (DEFERRED);
//     See RevenueCat's Proration Documentation for a full breakdown:
//     https://www.revenuecat.com/blog/engineering/google-proration/
//
// purchases.packagePrice(packageId, callback)
//   - Retrieves the price for a specific package.
//   - packageId: string
//
// purchases.showPaywall(shouldShowCloseButton, offeringId, callback)
//   - Displays a RevenueCat paywall.
//   - shouldShowCloseButton: boolean — may be overridden by the paywall template.
//   - offeringId: string — optional; shows this offering instead of the default.
//
// purchases.showPaywallIfNeeded(entitlementId, shouldShowCloseButton, offeringId, callback)
//   - Displays a paywall only if the user doesn't already have the specified
//     entitlement.
//   - entitlementId: string — required.
//   - shouldShowCloseButton: boolean — may be overridden by the paywall template.
//   - offeringId: string — optional; shows this offering instead of the default.
//
// purchases.getOfferings(callback)
//   - Retrieves all configured offerings and their available packages,
//     including live pricing — useful for building a fully custom purchase
//     UI without relying on RevenueCat's paywall templates.

// ============================================================================
// CALLBACK RESPONSE FIELDS - login
// ============================================================================
// resp.status: "SUCCESS" | "FAILED"
// resp.customerId: string — present on SUCCESS

// ============================================================================
// CALLBACK RESPONSE FIELDS - logout
// ============================================================================
// No response payload.

// ============================================================================
// CALLBACK RESPONSE FIELDS - customerId
// ============================================================================
// resp.status: "SUCCESS" | "FAILED"
// resp.customerId: string — present on SUCCESS
// resp.error: string | null — present on FAILED

// ============================================================================
// CALLBACK RESPONSE FIELDS - restore
// ============================================================================
// resp.status: "SUCCESS" | "FAILED"
// resp.customerId: string — present on SUCCESS
// resp.error: string | null — present on FAILED

// ============================================================================
// CALLBACK RESPONSE FIELDS - purchasePackage
// ============================================================================
// resp.status: "SUCCESS" | "CANCELLED" | "FAILED"
// resp.packageId: string — present on SUCCESS
// resp.error: string | null — present on FAILED
// Note: CANCELLED means the user backed out of the purchase flow — this is
// not an error and shouldn't be treated as one.

// ============================================================================
// CALLBACK RESPONSE FIELDS - packagePrice
// ============================================================================
// resp.status: "SUCCESS" | "FAILED"
// resp.packageId: string
// resp.price: number — e.g. 0.99
// resp.currency: string — e.g. "EUR"
// resp.localizedPrice: string — e.g. "€0.99"
// resp.error: string | null — present on FAILED

// ============================================================================
// CALLBACK RESPONSE FIELDS - showPaywall / showPaywallIfNeeded
// ============================================================================
// resp.status: "SUCCESS" | "FAILED"
// resp.message: "purchased" | "restored" | "cancelled" | "not_presented" | "error"
//   — describes the paywall outcome; can appear alongside either status.
// resp.error: string — present on some FAILED cases, e.g. "offering_not_found",
//   or "entitlement_id_missing" (showPaywallIfNeeded only, when entitlementId
//   is missing)

// ============================================================================
// CALLBACK RESPONSE FIELDS - getOfferings
// ============================================================================
// resp.status: "SUCCESS" | "FAILED"
// resp.offerings: object — keyed by offering identifier, each containing:
//   identifier: string
//   serverDescription: string
//   availablePackages: array of:
//     identifier: string — e.g. "$rc_monthly"
//     platformProductId: string — the real store product ID
//     productTitle: string
//     productDescription: string
//     price: number
//     currencyCode: string
//     localizedPriceString: string
// resp.error: string — present on FAILED

// --- In-App Purchases. Identity Management. Start ---

const userId = "sdfdsr-2345-3rsd-fsd3432";
const userEmail = "email@example.com";

const login_callback = function (resp) {
  if (resp.status === "SUCCESS") {
    console.log("Linked to RevenueCat customer:", resp.customerId);
  }
};

purchases.login(userId, userEmail, login_callback);

const logout_callback = function () {
  console.log("Logged out of RevenueCat.");
};

purchases.logout(logout_callback);

const customer_id_callback = function (resp) {
  if (resp.status === "SUCCESS") {
    console.log("Current customer ID:", resp.customerId);
  }
};

purchases.customerId(customer_id_callback);

// --- In-App Purchases. Identity Management. End ---


// --- In-App Purchases. Direct Purchase. Start ---

const packageId = "$rc_monthly";

const purchase_callback = function (resp) {
  if (resp.status === "SUCCESS") {
    console.log("Purchased package:", resp.packageId);
    // Call your backend to refresh entitlement state before unlocking access.
  } else if (resp.status === "CANCELLED") {
    console.log("User cancelled the purchase.");
  } else {
    console.log("Purchase failed:", resp.error);
  }
};

purchases.purchasePackage(packageId, purchase_callback);

// --- In-App Purchases. Direct Purchase. End ---


// --- In-App Purchases. Android Upgrade/Downgrade. Start ---

const newPackageId = "$rc_annual";
const oldProductId = "com.company.app.premium:monthly"; // retrieved from getOfferings()'s platformProductId field, not a package ID
const prorationMode = "immediateAndChargeProratedPrice"; // upgrading; use "deferred" for downgrades

const upgrade_callback = function (resp) {
  if (resp.status === "SUCCESS") {
    console.log("Plan changed to:", resp.packageId);
  }
};

purchases.purchasePackage(newPackageId, upgrade_callback, oldProductId, prorationMode);

// --- In-App Purchases. Android Upgrade/Downgrade. End ---


// --- In-App Purchases. Package Price. Start ---

const price_callback = function (resp) {
  if (resp.status === "SUCCESS") {
    console.log(resp.localizedPrice); // e.g. "€0.99"
  }
};

purchases.packagePrice(packageId, price_callback);

// --- In-App Purchases. Package Price. End ---


// --- In-App Purchases. Restore. Start ---

const restore_callback = function (resp) {
  if (resp.status === "SUCCESS") {
    console.log("Purchases restored for customer:", resp.customerId);
  }
};

purchases.restore(restore_callback);

// --- In-App Purchases. Restore. End ---


// --- In-App Purchases. Paywalls. Start ---

const paywall_callback = function (resp) {
  console.log(resp.status, resp.message);
  // e.g. "SUCCESS" "purchased", "FAILED" "cancelled", "FAILED" "not_presented"
};

const offeringId = "default"; // optional — omit to use the default offering
const showCloseButton = true; // may be overridden by the paywall template

purchases.showPaywall(showCloseButton, offeringId, paywall_callback);

const entitlementId = "premium"; // required

purchases.showPaywallIfNeeded(entitlementId, showCloseButton, offeringId, paywall_callback);

// --- In-App Purchases. Paywalls. End ---


// --- In-App Purchases. Get Offerings. Start ---

const offerings_callback = function (resp) {
  if (resp.status !== "SUCCESS") {
    console.log("Failed to fetch offerings:", resp.error);
    return;
  }

  const defaultOffering = resp.offerings["default"];
  defaultOffering.availablePackages.forEach((pkg) => {
    console.log(pkg.identifier, pkg.productTitle, pkg.localizedPriceString);
  });
};

purchases.getOfferings(offerings_callback);

// --- In-App Purchases. Get Offerings. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

{% hint style="warning" %}
Before using this prompt, gather your own project-specific values from RevenueCat: your **Package IDs** (e.g. `$rc_monthly`), **Entitlement ID** (e.g. `premium`), and **Offering ID** if you're not using the default. Include these in your feature description so the AI agent uses your actual values instead of placeholders.
{% endhint %}

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively In-App Purchases SDK: const purchases = new NativelyPurchases(); purchases.login(userId, userEmail, callback) links the app user to a RevenueCat customer — userId required, userEmail optional — returning resp.status ("SUCCESS" or "FAILED") and resp.customerId on success. purchases.logout(callback) reverts to an anonymous RevenueCat ID, no response payload. purchases.customerId(callback) retrieves the current customer ID, returning resp.status, resp.customerId on success, resp.error on failure. purchases.restore(callback) restores previous purchases, returning resp.status, resp.customerId on success, resp.error on failure — important on iOS. purchases.purchasePackage(packageId, callback, oldProductId, prorationMode) purchases a package — packageId required; oldProductId and prorationMode optional, Android-only, required together when upgrading/downgrading an active subscription (oldProductId must be the real store product ID from getOfferings()'s platformProductId field, not a package ID). prorationMode accepts: "immediateWithoutProration", "immediateWithTimeProration", "immediateAndChargeFullPrice", "immediateAndChargeProratedPrice", or "deferred" (typically "immediateAndChargeProratedPrice" for upgrades, "deferred" for downgrades). Returns resp.status ("SUCCESS", "CANCELLED", or "FAILED"), resp.packageId on success, resp.error on failure. purchases.packagePrice(packageId, callback) retrieves pricing, returning resp.status, resp.price (number), resp.currency, resp.localizedPrice on success. purchases.showPaywall(shouldShowCloseButton, offeringId, callback) and purchases.showPaywallIfNeeded(entitlementId, shouldShowCloseButton, offeringId, callback) display a RevenueCat paywall (the second only if the user lacks the given entitlement; entitlementId required for that one) — returning resp.status, resp.message ("purchased"/"restored"/"cancelled"/"not_presented"/"error"), and resp.error on some failures. purchases.getOfferings(callback) retrieves all offerings and packages with live pricing, returning resp.status and resp.offerings (keyed by offering identifier, each with availablePackages containing identifier, platformProductId, productTitle, productDescription, price, currencyCode, localizedPriceString). For reference: https://docs.buildnatively.com/natively-platform/features/purchases.md
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Add a paywall that shows monthly and annual subscription options, links to the user's account on login, and lets them restore purchases".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Know when Stripe is and isn't allowed

Apple and Google require digital goods, content, and subscriptions consumed *within* the app to go through their native purchase systems (IAP) - using Stripe or another processor for these will get your app rejected. The main exception is *physical goods and real-world services* (e.g. a taxi ride, a hotel booking, a physical product) - these can use Stripe or any other payment processor, even inside the app.

#### Only show IAP UI inside the Natively app

Use [Browser Info](/guides/integration/browser-info) to detect `isNativeApp`, and only show purchase/paywall UI when `true`. In a regular browser, use your web checkout flow instead (e.g., Stripe).

#### Log in before purchasing, restoring, or fetching offerings

Call `login` with a stable internal user ID as soon as the user is authenticated in your app - ideally the same ID you'd use across web and mobile. This ties purchases to the right customer in RevenueCat from the start, rather than to an anonymous ID that's harder to reconcile later.

#### Never trust the client alone for entitlement access

A `SUCCESS` purchase or restore callback confirms the purchase flow completed - it doesn't guarantee your backend has verified the entitlement yet. Always re-check subscription status server-side (via RevenueCat's REST API, or your own backend synced with RevenueCat) before unlocking premium features.

#### Handle `CANCELLED` differently from `FAILED`&#x20;

A cancelled purchase means the user backed out - this isn't an error and usually shouldn't show error messaging. Reserve error UI for genuine `FAILED` results.

#### Always offer Restore Purchases, especially on iOS

Users reinstalling the app or switching devices need a way to recover purchases tied to their App Store / Google Play account without paying again.

#### Don't confuse Package ID with Store Product ID

`packageId` (e.g. `$rc_monthly`) is what you pass to `purchasePackage` for a normal purchase. But `oldProductId`, required for Android upgrades/downgrades, must be the real store product ID (e.g. `com.company.app.premium:monthly`) - retrieve it from `platformProductId` in `getOfferings()`'s response, not from a package ID.

#### Don't hardcode prices or product info

Use `getOfferings()` or `packagePrice()` to display live, localized pricing - prices vary by region and can change without a client update.

### How to verify a user's subscription

Never rely on a purchase callback alone as proof of entitlement - always confirm with RevenueCat's own record of the customer's subscription state.

{% tabs %}
{% tab title="Bubble.io Plugin" %}
Use the **RevenueCat - Verify Subscription** action (documented in [Setup Logic](#bubble.io-plugin-1)), which checks a customer's entitlement directly using your RevenueCat Secret API Key.

#### Setup example:

1. **On each user login or first signup**, set the RevenueCat Customer ID to your own user's unique ID using the **Set Customer ID** action.
2. **Purchase a subscription (package)** using the **Purchase Package** action.
3. **Verify the user's subscription** whenever you need to check their entitlement status, using the **RevenueCat - Verify Subscription** action.
4. **If purchases aren't working**, check the **Latest Error** state on the Natively - Purchases element for details.
5. You can also use the [RevenueCat API](https://www.revenuecat.com/reference/basic) directly for other purposes, such as independent validation of purchases.

<div><figure><img src="/files/HW3stoOWJNNcesmmPNhD" alt=""><figcaption><p>Step 1</p></figcaption></figure> <figure><img src="/files/EcbiHw5snTEclc9GoDxq" alt=""><figcaption><p>Step 2</p></figcaption></figure> <figure><img src="/files/Son2BW8vVXsE1mTJin5p" alt=""><figcaption><p>Step 3.1</p></figcaption></figure> <figure><img src="/files/6zi4iU8Hp2rFv4kArbdD" alt=""><figcaption><p>Step 3.2</p></figcaption></figure> <figure><img src="/files/9BPMmmrmIiYxcYNN1lhD" alt=""><figcaption><p>Step 4</p></figcaption></figure></div>
{% endtab %}

{% tab title="Backend (REST API)" %}
Fetch the customer from [RevenueCat's REST API](https://www.revenuecat.com/docs/api-v1?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner#tag/customers/operation/subscribers) using your Secret API Key and check whether the relevant entitlement is active.

{% hint style="info" %}
This action requires your RevenueCat **Secret API Key** - a different key from the Public API Keys used in Natively Dashboard Setup. Find it under **RevenueCat App** > **API keys** > **Secret API keys**, and confirm you're using an **API Key V1** - other versions won't work.
{% endhint %}

```http
GET https://api.revenuecat.com/v1/subscribers/{app_user_id}
Authorization: Bearer YOUR_SECRET_API_KEY
```

{% hint style="danger" %}
This call must always happen server-side. Never expose your **Secret API Key** in frontend code - anyone with it could query or manipulate subscriber data for your entire project.
{% endhint %}

The response includes a `subscriber.entitlements` object, keyed by entitlement identifier. Check whether the entitlement you care about is present, and its `expires_date` is null or in the future:

```json
{
  "subscriber": {
    "entitlements": {
      "premium": {
        "expires_date": "2026-08-01T00:00:00Z",
        "product_identifier": "com.company.app.premium:monthly",
        "purchase_date": "2026-07-01T00:00:00Z"
      }
    }
  }
}
```

{% hint style="info" %}
For a more real-time, scalable setup, RevenueCat also supports webhooks that notify your backend the moment a subscription's state changes - useful if you want to keep your own database in sync rather than checking RevenueCat on every request. See [RevenueCat's webhook](https://www.revenuecat.com/docs/integrations/webhooks?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) documentation if you want to build this.
{% endhint %}
{% endtab %}
{% endtabs %}

## Testing

{% hint style="danger" %}
Never use real payment methods to test purchases - always use sandbox/test accounts.
{% endhint %}

**Test on Android** - follow [RevenueCat's Testing purchases in Play Store Sandbox](https://www.revenuecat.com/docs/test-and-launch/sandbox/google-play-store?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide for a full walkthrough.

**Test on iOS** - follow [RevenueCat's Testing purchases in App Store Sandbox](https://www.revenuecat.com/docs/test-and-launch/sandbox/apple-app-store?utm_medium=referral\&utm_source=solp\&utm_campaign=natively\&utm_content=partner) guide for a full walkthrough.

## Troubleshooting

<details>

<summary>Purchases fail, or the paywall doesn't open</summary>

Go through this checklist:

* **Check the Debug Console.** Open the [Debug Console](/guides/integration/debug-console) to inspect the actual error.
* **Confirm you're using the correct Public API Key.** Use your **App Store** (`appl_...`) or **Play Store** (`goog_...`) key in the Natively Dashboard - not the RevenueCat **Test Store** key.
* **Confirm iOS payment setup is fully complete.** Sandbox purchases fail if your Tax and Banking forms in App Store Connect aren't **Clear**, even with a signed Paid Applications Agreement.
* **Confirm Play Service Credentials have propagated.** New Google Play Service Account credentials can show "Invalid Play Store credentials" for up to 36 hours after being generated.

</details>

<details>

<summary>Restore Purchases returns success, but the website doesn't reflect the access</summary>

A successful restore only confirms that RevenueCat recognized prior purchases; your app must still call your backend to re-verify entitlement status and update the UI accordingly. Don't unlock features directly from the restore callback alone.

</details>

<details>

<summary>Android upgrade/downgrade fails, or Google Play shows a duplicate subscription instead of a plan change</summary>

Confirm `oldProductId` is the real store Product ID (e.g. `com.company.app.premium:monthly`), not a package ID like `$rc_monthly`. Retrieve the correct value from `platformProductId` in `getOfferings()`'s response or from the RevenueCat **Products** section.

</details>

<details>

<summary>Not working in a web browser or the Natively Preview app</summary>

In-App Purchases is a native feature and does not work in a standard web browser or in the Natively Preview app. Test on a real device using a full Natively build.

</details>

[^1]: Replace this placeholder


# Microphone

Grant your app access to the device microphone for audio and video recording.

## What is the Microphone?

The Microphone feature gives your app access to the device's microphone, which other features rely on to function - [Audio Recorder](/guides/integration/audio-recorder) uses it to capture voice recordings, and [Camera](/natively-platform/features/camera) uses it when recording video with sound. The Microphone itself has no direct SDK - it's a permission gate that other features build on top of.

Requesting microphone access prompts the user for permission the first time it's used, on both iOS and Android.

## Prerequisites

{% hint style="success" %}
This feature is available in any **paid** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Natively Dashboard Setup

{% hint style="success" %}
Microphone is automatically enabled for all new apps. You only need to visit this section if you want to disable it or update the Permission Description.
{% endhint %}

1. Open your Natively app dashboard and navigate to **Features > Microphone**.
2. Toggle the feature to **Enabled**.
3. Enter the **Permission Description**.
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to users when the OS asks them to grant **microphone** access. Explain clearly why your app needs this permission - for example: "We use your microphone to let you record voice messages."
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Instead of an Initialization/Setup Logic pair, this section lists the features that rely on **Microphone** access and how each one uses it.

### Features that use Microphone

{% content-ref url="/pages/Y1HqajhJmeuW4ELmDJaW" %}
[Camera](/natively-platform/features/camera)
{% endcontent-ref %}

{% content-ref url="/pages/zmSrGXzn229lcXxxsFoZ" %}
[Audio Recorder](/guides/integration/audio-recorder)
{% endcontent-ref %}

### How to use

#### Enable Microphone before implementing either feature

Since neither Audio Recorder nor Camera's video recording can request permission on their own if Microphone is disabled, always confirm Microphone is enabled in the dashboard before building on top of it.

#### Write one Permission Description that fits every use case in your app

If you use both Audio Recorder and Camera video recording, make sure the Permission Description covers the full scope of why your app needs microphone access, not just one feature's use case.

#### Test the permission prompt path end-to-end

Trigger whichever dependent feature (Audio Recorder or Camera video) is reachable first in your app's flow, and confirm the system prompt appears with your configured description before relying on it in production.

## Troubleshooting

<details>

<summary>Microphone permission denied</summary>

If the user previously denied microphone access, no dependent feature (Audio Recorder, Camera video) will be able to prompt again automatically. Direct them to [Open App Settings](/guides/integration/open-app-settings) to manually re-enable the permission.

</details>

<details>

<summary>Permission Description causes App Store rejection</summary>

Apple requires a clear, specific explanation for microphone access. Avoid generic text like "we need this to improve the app" - describe the actual use case (e.g., "We use your microphone to let you record voice messages").

</details>


# NFC

Read and write NFC tags directly from your app using native NFC capabilities.

## What is NFC?

NFC (Near Field Communication) lets your app interact with NFC tags - small chips embedded in cards, stickers, or objects. Your app can read data from NFC tags or write data to them, enabling use cases like contactless check-ins, product authentication, smart packaging, loyalty cards, and more.

{% hint style="warning" %}
Natively supports standard, writable, NDEF-formatted NFC tags - specifically **NFC Forum Type 2 Tags** (including NTAG213, NTAG215, NTAG216, and MIFARE Ultralight) and **NFC Forum Type 5 Tags** (ISO15693 NDEF tags). Natively has directly tested NTAG213, NTAG215, and NTAG216; other NDEF-formatted tags following these standards are expected to work but haven't been individually verified.

**Not supported:** payment cards, access cards, MIFARE Classic cards, DESFire applications, and other proprietary or non-NDEF smart cards.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

**iOS only:**

* An [Apple Developer](https://developer.apple.com/account) account.
* Your iOS app is already [published](/natively-platform/app-info/ios-build) in the Natively Dashboard.

## App Capabilities Configuration (iOS only)

{% hint style="info" %}
App capabilities define what system-level features your app is allowed to use on iOS. Before Natively can include the NFC feature in your iOS build, the **NFC Tag Reading** capability must be explicitly enabled in your Apple Developer account for your app's Bundle ID.
{% endhint %}

1. Open your [Apple Developer account](https://developer.apple.com/account) and navigate to [Certificates, IDs & Profiles > Identifiers](https://developer.apple.com/account/resources/identifiers/list).
2. Select your app's Bundle ID.
3. Scroll down the **Capabilities** list and enable **NFC Tag Reading.**
4. Click **Save** and confirm.

## Natively Dashboard Setup

{% hint style="info" %}
Before proceeding with the iOS app, make sure you have completed the [App Capabilities Configuration (iOS only)](#app-capabilities-configuration-ios-only) step above.
{% endhint %}

1. Open your Natively app dashboard and navigate to **Features > NFC**.
2. Toggle the feature to **Enabled**.
3. Enter the **Permission Description**.
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to users when the OS asks them to grant **NFC** access. Explain clearly why your app needs this permission - for example: "This app uses NFC to read and write contactless cards."
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - NFC

#### **Fields:**

* **Read Alert Message** - message shown to the user when the NFC scanner is waiting for a tag to be placed near the device.
* **Write Alert Message** - message shown to the user when the NFC scanner is waiting for a tag to be placed near the device for writing.
* **Read Detected Message** - message shown when a tag is successfully detected and read.
* **Write Detected Message** - message shown when a tag is successfully detected and data has been written.
* **nfcresponse** - a predefined Bubble type `Natively - NFCResponse` that needs to be selected when working with NFC tag data.

#### **Events:**

* **NFC Read Failed** - fires when a read attempt fails.
* **NFC Write Failed** - fires when a write attempt fails.
* **NFC Write Success** - fires when data is successfully written to a tag.
* **NFC Read Success** - fires when a tag is successfully read.
* **NFC Ready To Use** - fires when the **NFC Is Available** state is received (useful for Android to confirm NFC is enabled on the device).

#### **States:**

* **Error** - error message if a read or write attempt failed.
* **Result** - the NFC tag result data.
* **NFC Tag ID** - the ID of the detected NFC tag.
* **NFC Tag Type** - the type of the detected NFC tag.
* **NFC Is Available** - Yes/No. Whether NFC is available on the device.

#### **Actions:**

* Read Data - starts an NFC read scanning session.
* Write Data - starts an NFC write scanning session:
  * **Record Data** - the data to write to the tag (text or URL). To open your app when the tag is scanned, use your Bundle ID scheme — for example: `com.bundle.id://open?url={url}` .
  * **Record ID** - a string identifier stored alongside the record on the tag. Only one record can currently be stored per tag, so this doesn't yet enable reading or writing multiple records.
  * **Record Data Type** - `text` or `uri`. When set to `uri`, scanning the written tag will automatically open the link in the device's default browser.
    {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY NFC - DOCUMENTATION & EXAMPLES
// ============================================================================

const readAlertMessage = "Hold your device near an NFC tag";
const writeAlertMessage = "Hold your device near an NFC tag to write";
const readDetectedMessage = "Tag detected!";
const writeDetectedMessage = "Tag written successfully!";

const nfcService = new NativelyNFCService(
  readAlertMessage,
  writeAlertMessage,
  readDetectedMessage,
  writeDetectedMessage
);

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// nfcService.available(callback)
//   - Checks whether NFC is available on the device.
//
// nfcService.read(callback)
//   - Starts a single NFC read session. Performs one read, then the session
//     ends — call read() again to scan another tag.
//
// nfcService.write(recordId, recordData, recordDataType, callback)
//   - Starts an NFC write session.
//   - recordId: string — identifier stored alongside the record. Only one
//     record can currently be stored per tag.
//   - recordData: string — the data to write (text or URL). To open your
//     app when the tag is scanned, use your Bundle ID scheme:
//     com.bundle.id://open?url={url}
//   - recordDataType: string — "text" or "uri". When "uri", scanning the
//     written tag automatically opens the link in the browser.

// ============================================================================
// CALLBACK RESPONSE FIELDS - available
// ============================================================================
// resp.status: boolean — true if NFC is available, false if not

// ============================================================================
// CALLBACK RESPONSE FIELDS - read
// ============================================================================
// resp.status: "SUCCESS" | "FAILED"
// resp.message: string — present if status is "FAILED"
// Note: the Detected Message may currently fire before a read has actually
// succeeded — always check resp.status rather than relying on it alone.

// ============================================================================
// CALLBACK RESPONSE FIELDS - write
// ============================================================================
// resp.type_format: string — e.g. "nfcWellKnown"
// resp.type: string
// resp.id: string
// resp.length: number
// resp.payload_content: string — the data written to the tag
// resp.payload_raw: string
// resp.status: "SUCCESS" | "FAILED"
// resp.message: string — present if status is "FAILED"

// --- NFC. Check Availability Example. Start ---

const available_callback = function (resp) {
  if (resp.status) {
    console.log("NFC is available");
  } else {
    console.log("NFC is not available on this device");
  }
};

nfcService.available(available_callback);

// --- NFC. Check Availability Example. End ---


// --- NFC. Read Example. Start ---

const read_callback = function (resp) {
  if (resp.status === "SUCCESS") {
    console.log("NFC tag read successfully:", resp);
  } else {
    console.log("Read failed:", resp.message);
  }
};

nfcService.read(read_callback);
// Call nfcService.read(read_callback) again to scan another tag

// --- NFC. Read Example. End ---


// --- NFC. Write Example. Start ---

const recordId = "1"; // stored alongside the record — only one record per tag currently
const recordData = "https://yourapp.com";
const recordDataType = "uri"; // "text" or "uri"

const write_callback = function (resp) {
  if (resp.status === "SUCCESS") {
    console.log("Written data:", resp.payload_content);
  } else {
    console.log("Write failed:", resp.message);
  }
};

nfcService.write(recordId, recordData, recordDataType, write_callback);

// --- NFC. Write Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively NFC SDK: const nfcService = new NativelyNFCService(readAlertMessage, writeAlertMessage, readDetectedMessage, writeDetectedMessage); readAlertMessage/writeAlertMessage are shown while waiting for a tag, readDetectedMessage/writeDetectedMessage are shown when a tag is detected (note: the detected message may currently fire before the read/write has actually succeeded — always check resp.status). nfcService.available(callback) checks if NFC is available, returning resp.status (boolean). nfcService.read(callback) starts a single NFC read session — it performs one read and ends, call it again to scan another tag — returning resp.status ("SUCCESS" or "FAILED") and resp.message on failure. nfcService.write(recordId, recordData, recordDataType, callback) starts an NFC write session — recordId: string, stored alongside the record, but only one record can currently be stored per tag; recordData: string, text or URL; recordDataType: "text" or "uri" (uri opens the link in the browser when the tag is scanned) — returning resp.payload_content (the written data), resp.status ("SUCCESS" or "FAILED"), and resp.message on failure. For reference: https://docs.buildnatively.com/natively-platform/features/nfc
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "When the user taps the scan button, read an NFC tag and display the tag content on the screen".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Always check availability first

Call `nfcService.available()` before showing any NFC-related UI to confirm the device supports NFC. On Android, the user may also need to enable NFC in their device settings - handle this gracefully by showing a message directing them to do so if NFC is unavailable.

#### Use descriptive alert messages

The alert messages shown during scanning sessions are the only feedback the user gets while waiting for a tag. Make them clear and actionable - for example: "Hold your phone near the card to scan" rather than a generic message.

#### Read vs Write

Use `read` to retrieve data from an existing NFC tag - for example, to authenticate a product, check in a user, or retrieve a URL. Use `write` to program a new or blank tag with data your app provides.

#### Only one record can be stored per tag

`recordId` is stored alongside the data you write, but tags currently support a single record - you can't write multiple records to the same tag and read them back individually.

#### Use `uri` type to open links automatically

When writing a tag with `recordDataType: "uri"`, scanning that tag on any NFC-enabled device will automatically open the URL in the device's default browser - even outside your app. This is useful for product cards, event badges, or any physical object that should direct users to a web page.

#### Use your Bundle ID scheme to open your app from a tag

To make a scanned tag open your app directly, write your app's Bundle ID scheme as the Record Data - for example `com.example.app://open?url=https://yourapp.com`. This works in combination with Universal Links and the `uri` data type.

#### Supported NFC tags

The app supports NDEF-formatted tags following the NFC Forum Type 2 (NTAG213, NTAG215, NTAG216, MIFARE Ultralight) and Type 5 (ISO15693) standards. Proprietary or non-NDEF cards - payment cards, access cards, MIFARE Classic, DESFire - are not supported.

## Troubleshooting

<details>

<summary>NFC is not working on the device</summary>

On Android, the user may need to enable NFC in their device settings before it can be used. The exact location varies by manufacturer - typically found under **Settings** > **Connected devices** > **NFC**. Use `nfcService.available()` to check if NFC is enabled before attempting to read or write.

</details>

<details>

<summary>NFC is not available on the iOS simulator or Android emulator</summary>

NFC requires a physical device with NFC hardware. It will not work on simulators or emulators. Test on a real device only.

</details>

<details>

<summary>Tag not detected during read or write</summary>

Make sure you are using a supported tag type - only **NTAG213, NTAG215, and NTAG216 (MiFare Ultralight)** have been tested and confirmed to work. Hold the device steady and close to the tag during scanning.

</details>

<details>

<summary>Write succeeds, but scanning the tag doesn't open the URL</summary>

Make sure `recordDataType` is set to `uri` and that the URL is valid and complete (including `https://`). If `recordDataType` is set to `text`, the tag content will be read as plain text and won't trigger a browser open.

</details>

<details>

<summary>App not opening when tag is scanned</summary>

Verify that the Bundle ID scheme is correctly formatted in the Record Data - for example `com.example.app://open?url=https://yourapp.com`. On iOS, Universal Links may need to be configured for the deep link to work correctly.

</details>

[^1]: Replace this placeholder


# Notifications

Send timely alerts to your users, even when your app isn't open.

## What are Notifications?

Push Notifications let your app reach users on their lock screen or in their notification tray, whether the app is open, backgrounded, or fully closed. You can use them to bring users back after a delay, alert them to new activity, or deliver time-sensitive updates - order confirmations, chat messages, reminders, and more.

## Supported providers

**OneSignal** - the recommended option for most apps. Handles device registration, targeting, and delivery through a dashboard-driven setup, with minimal backend work required.

{% content-ref url="/pages/k9MsZswnHE6zIyEKzAn0" %}
[OneSignal - Push Notifications](/natively-platform/features/notifications/onesignal-push-notifications)
{% endcontent-ref %}

**Firebase (Advanced)** - for teams that need direct control over Firebase Cloud Messaging, or already have a Firebase-based backend and want to send notifications without going through OneSignal.

{% content-ref url="/pages/pMcjU4MdcfrIzYtmaYVN" %}
[Firebase - Push Notifications (Advanced)](/natively-platform/features/notifications/firebase-push-notifications-advanced)
{% endcontent-ref %}

{% hint style="info" %}
Most apps should use OneSignal. Reach for Firebase only if you have a specific reason to manage the messaging infrastructure yourself.
{% endhint %}


# OneSignal - Push Notifications

Reach your users with timely push notifications, powered by OneSignal.

## What are OneSignal Push Notifications?

Natively provides built-in support for [OneSignal](https://onesignal.com/) to handle push notification delivery for your app. OneSignal manages device registration and targeting through its own dashboard, and you send notifications either directly from the OneSignal dashboard or via the OneSignal REST API.

{% hint style="warning" %}
We do not currently support Rich Push Notifications (notifications containing action buttons or images). This feature will be added soon.
{% endhint %}

{% embed url="<https://youtu.be/C2wyJIFsKYc>" %}

## Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* A [OneSignal](https://onesignal.com/) account.
* **iOS:**
  * An [Apple Developer](https://developer.apple.com/account) account.
  * Your iOS app is already [published](/natively-platform/app-info/ios-build) in the Natively Dashboard.
* **Android:**
  * A [Firebase](https://firebase.google.com/) account.
  * Your Android app is already [published](/natively-platform/app-info/android-build) in the Natively Dashboard.

## OneSignal Configuration

### Pre-OneSignal Configuration

{% tabs %}
{% tab title="Android" %}
To send push notifications to Android devices, OneSignal requires **Firebase Cloud Messaging (FCM) credentials**.

{% stepper %}
{% step %}

#### Create or open your Firebase Project

1. Go to the [Firebase console](https://console.firebase.google.com/).
2. If you don’t have a project yet, click **Create a project** and complete the setup.
3. If you already have a project, **select it**.
   {% endstep %}

{% step %}

#### Confirm Firebase Cloud Messaging API v1 is enabled

While in the **Project Overview** page, select **Settings > General** from the left sidebar, then go to the **Cloud Messaging** tab.

{% hint style="success" %}
**Firebase Cloud Messaging API (V1)** is enabled by default for most projects.&#x20;
{% endhint %}

If it shows as disabled:

1. Click on the 3-dots menu.
2. Click **Manage the API in Google Cloud Console**.
3. Click **Enable** in the Google Cloud Console. Wait a few minutes for the change to reflect in Firebase.
   {% endstep %}

{% step %}

#### Generate a Service Account JSON file

While on the **Project Overview** page, select **Settings > General** from the left sidebar, then go to the **Service Accounts** tab:

1. At the bottom, click **Generate new private key**.
2. Confirm by clicking **Generate key** in the popup.
3. **Save** the `.json` file in a secure location.&#x20;

You'll need it for [OneSignal Dashboard Configuration](#onesignal-dashboard-configuration).
{% endstep %}
{% endstepper %}
{% endtab %}

{% tab title="iOS" %}
To send push notifications to iOS devices, OneSignal requires the **Push Notifications** **App Capability** enabled on your Bundle ID and an **APNs authentication key** from your Apple Developer account.

{% stepper %}
{% step %}

#### App Capabilities Configuration

{% hint style="info" %}
App capabilities define what system-level features your app is allowed to use on iOS. Before Natively can include the **OneSignal Push Notifications** feature in your build, the **Push Notifications** capability must be explicitly enabled in your Apple Developer account for your app's Bundle ID.
{% endhint %}

1. Open your [Apple Developer](https://developer.apple.com/account) account and navigate to [Certificates, IDs & Profiles > Identifiers](https://developer.apple.com/account/resources/identifiers/list).
2. Select your app's Bundle ID.
3. Scroll down the **Capabilities** list and enable **Push Notifications**.
4. Click **Save** and confirm.
   {% endstep %}

{% step %}

#### Set up APNs Authentication Key

1. Open your [Apple Developer](https://developer.apple.com/account) account and navigate to [Certificates, IDs & Profiles > Keys](https://developer.apple.com/account/resources/authkeys/list).
2. Click **+** to register a new key.
3. Enter **Key Name**.
4. Select the **Apple Push Notifications service (APNs)** checkbox and click **Configure**.
5. In the **Environment** dropdown menu, select **Sandbox & Production** and click **Save**.
6. Click **Continue**, then **Register**.
7. **Download** the generated `.p8` key.

{% hint style="warning" %}
You can only download this file once, so save it securely. If you lose it, you'll need to revoke the key and generate a new one.
{% endhint %}
{% endstep %}

{% step %}

#### Gather your credentials

Collect:

* **Key ID** - located in the row of the key you just created. Make sure it matches the downloaded `.p8` file.
* **Team ID** - located in your [Apple Developer](https://developer.apple.com/account) account under **Membership details**.
* **Bundle ID** - located in your Natively Dashboard under **Publish** > **iOS Build** > **Bundle Identifier**.

You'll need these for [OneSignal Dashboard Configuration](#onesignal-dashboard-configuration).

<figure><img src="https://mintcdn.com/onesignal/9_Q1FZLh6C0BFLq-/images/docs/c8d0207041e7c277f7e4ca49a3f6100280ddcbf970fce9b720fddfbda6683bb6-p8.png?fit=max&#x26;auto=format&#x26;n=9_Q1FZLh6C0BFLq-&#x26;q=85&#x26;s=fc9c689b0880bda69813428ee7fdbe5f" alt=""><figcaption><p>Credentials to collect</p></figcaption></figure>
{% endstep %}
{% endstepper %}
{% endtab %}
{% endtabs %}

### OneSignal Dashboard Configuration

{% stepper %}
{% step %}

#### Create OneSignal App

1. Navigate to your [OneSignal dashboard](https://app.onesignal.com/apps) and click **New App/Website**.
2. Enter your application's name in the **OneSignal App Name** field.
3. Select the option to create a new organization and enter its name.
4. Choose either the **Apple iOS (APNs)** or **Google Android (FCM)** channel.
5. Click **Next**.

{% hint style="info" %}
If you have both an iOS and Android app for one website, you only need one OneSignal application with both platforms enabled.&#x20;

You can add the second platform later in your OneSignal app dashboard under **Settings** > **Push & In-App**.
{% endhint %}
{% endstep %}

{% step %}

#### Configure your platform

{% tabs %}
{% tab title="Google Android (FCM)" %}

1. Click **Select file** under **Service Account JSON** and upload the `.json` file you downloaded in the [Generate a Service Account JSON file](#android) step above.
2. Click **Save & Continue**.
3. In the **Select your target SDK**, select **Android Native**.
4. Click **Save & Continue**.
5. Copy your **App ID** and click **Done**.

{% hint style="warning" %}
The **OneSignal App ID** is the same for both iOS and Android.&#x20;

You can find it later in your OneSignal app dashboard under **Settings** > **Keys & IDs** > **OneSignal App ID**.
{% endhint %}
{% endtab %}

{% tab title="Apple iOS (APNs)" %}

1. Choose **.p8 Auth Key (Recommended)** under **APNs Authentication Type**.
2. Provide the following:
   * `.p8` **file** - the private key file you downloaded in the [Set up APNs Authentication Key](#ios) step above.
   * **Key ID**, **Team ID**, and **App Bundle ID** - gathered in the [Gather your credentials](#ios) step above.
3. Click **Save & Continue**.
4. In **Select your target SDK**, select **Native iOS**.
5. Click **Save & Continue**.
6. Copy your **App ID** and click **Done**.

{% hint style="warning" %}
The **OneSignal App ID** is the same for both iOS and Android.&#x20;

You can find it later in your OneSignal app dashboard under **Settings** > **Keys & IDs** > **OneSignal App ID**.
{% endhint %}

{% hint style="info" %}
When configuring Apple iOS (APNs), you'll see an **Enable iOS 12 direct to history** checkbox. This controls whether **iOS Provisional Notifications** are enabled - notifications that can appear quietly in Notification Center before the user explicitly grants permission. This setting lives entirely in OneSignal's dashboard, not in the Natively Dashboard. See [OneSignal's Provisional Push Notifications docs](https://documentation.onesignal.com/docs/en/ios-provisional-push-notifications) for details.
{% endhint %}
{% endtab %}
{% endtabs %}
{% endstep %}
{% endstepper %}

## Natively Dashboard Setup

Before proceeding, make sure you have completed the [OneSignal Configuration](#onesignal-configuration) steps above.

1. Open your Natively app dashboard and navigate to **Features** > **Notifications** > **OneSignal**.
2. Toggle the feature to **Enabled**.
3. Enter the **OneSignal App ID**.
4. Enter the **Permission Description**.
5. *(Optional)* Adjust the **Automatically request push permission on app launch** checkbox.
6. Click **Save**.
7. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to users when the OS asks them to grant **notification** access. Explain clearly why your app needs this permission - for example: "We'll send you push notifications when your order ships."
{% endhint %}

{% hint style="info" %}
The **Automatically request push permission on app launch** is enabled by default, triggering the system permission prompt immediately after the user launches the app. Disable it if you'd rather request permission later, at a more relevant moment in your app's flow.
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### &#x20;Setup logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Push Notifications (OneSignal)

{% hint style="warning" %}
This element must be set to **Visible on page load** to initialize correctly. It should be placed directly on the page root and not inside hidden containers, such as Popups, Floating Groups, Group Focus elements, or Repeating Groups. To hide the element from your UI, you may set its dimensions to 0x0 px.
{% endhint %}

#### Events:

* **OneSignal Player ID Updated** - fires whenever the Player ID is successfully retrieved from OneSignal.
* **Permissions Authorized** - fires when the user taps **Allow** on the native system permission dialog.
* **Permissions Denied** - fires when the user taps **Don't Allow** on the native system permission dialog.
* **Permission Status Updated** - fires whenever the device's notification settings change.
* **External ID Updated** - fires after an External ID is successfully set, updated, or removed.
* **External ID Error** - fires if an operation involving an External ID fails.

#### States:

* **Permission Status** - Yes/No. `Yes` if the user has granted notification permissions.
* **OneSignal PlayerId** - the unique identifier for the current device, used to target this specific user (equivalent to the Subscription ID under **Audience** > **Subscriptions** in your OneSignal dashboard).
* **OneSignal ExternalId** - the unique identifier for the current user, used to target this specific user.
* **Error message** - the text of the error encountered during a failed operation (e.g. a failed External ID update).

#### Actions:

* **Request the user's push notification permission** - triggers the native system dialog using the Permission Description defined in your dashboard:
  * **Open Settings** - if permission was previously denied, displays a native alert offering to take the user directly to system settings to enable notifications.
* **Get the user's OneSignal PlayerId** - manually refreshes the **OneSignal Player ID** state.
* **Get the user's push notification permission status** - re-checks the device's notification settings and updates the **Permission Status** state.
* **Get the user's External ID** - fetches the custom ID currently associated with this Player ID in OneSignal.
* **Set the user's External ID** - links a custom ID to this OneSignal Player ID.
* **Remove the user's External ID** - unlinks the custom ID from the current Player ID.
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY NOTIFICATIONS (ONESIGNAL) - DOCUMENTATION & EXAMPLES
// ============================================================================

const notifications = new NativelyNotifications();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// notifications.getPermissionStatus(callback)
//   - Checks if the user has currently granted push notification permissions.
//
// notifications.requestPermission(fallbackToSettings, callback)
//   - Prompts the native OS permission dialog.
//   - fallbackToSettings: boolean — if true, prompts the user to open OS settings if previously denied.
//
// notifications.getOneSignalId(callback)
//   - Retrieves the unique OneSignal Player ID for this device.
//   - In the OneSignal dashboard, this is labeled as the Subscription ID.
//
// notifications.getExternalId(callback)
//   - Fetches the custom External ID linked to this Player ID in OneSignal.
//
// notifications.setExternalId({ externalId }, callback)
//   - Links a custom ID (e.g. your database User ID) to this Player ID in OneSignal.
//
// notifications.removeExternalId(callback)
//   - Unlinks the current custom External ID from this Player ID.

// ============================================================================
// CALLBACK RESPONSE FIELDS - getPermissionStatus
// ============================================================================
// resp.status: boolean — true if allowed, false if denied/undetermined

// ============================================================================
// CALLBACK RESPONSE FIELDS - requestPermission
// ============================================================================
// resp.status: boolean — true if the user just tapped Allow, false if Don't Allow

// ============================================================================
// CALLBACK RESPONSE FIELDS - getOneSignalId
// ============================================================================
// resp.playerId: string — the device's OneSignal Player ID (e.g. "a301a5b5-ac6e-...")

// ============================================================================
// CALLBACK RESPONSE FIELDS - getExternalId / setExternalId
// ============================================================================
// resp.externalId: string — the linked External ID, if present
// resp.error: string — present if the operation failed
// resp.message: string — present if the operation failed

// ============================================================================
// CALLBACK RESPONSE FIELDS - removeExternalId
// ============================================================================
// A null/empty response indicates the External ID was successfully removed.
// resp.error: string — present if the operation failed
// resp.message: string — present if the operation failed

// --- Push Notifications. Permissions & Player ID. Start ---

const permission_check_callback = function (resp) {
  console.log(resp.status); // true if allowed, false if denied/undetermined
};

const permission_request_callback = function (resp) {
  console.log(resp.status); // true if the user just tapped Allow, false if Don't Allow
};

const player_id_callback = function (resp) {
  console.log(resp.playerId); // e.g. "a301a5b5-ac6e-..."
};

const fallbackToSettings = false; // true shows a custom alert offering to open OS settings if previously denied

notifications.getPermissionStatus(permission_check_callback);
notifications.requestPermission(fallbackToSettings, permission_request_callback);
notifications.getOneSignalId(player_id_callback);

// --- Push Notifications. Permissions & Player ID. End ---


// --- Push Notifications. External ID Management. Start ---

notifications.getExternalId((resp) => {
  const res = Array.isArray(resp) && resp.length > 0 ? resp[0] : null;

  if (res && res.externalId) {
    console.log("Current External ID:", res.externalId);
  } else {
    console.warn((res && (res.error || res.message)) || "No External ID found.");
  }
});

notifications.setExternalId({ externalId: "user_db_id_12345" }, (resp) => {
  if (resp && resp.externalId) {
    console.log("External ID set successfully to:", resp.externalId);
  } else {
    console.error((resp && (resp.error || resp.message)) || "Failed to set External ID.");
  }
});

notifications.removeExternalId((resp) => {
  if (resp && (resp.error || resp.message)) {
    // Failure — an error or message is present
    console.error("Failed to remove External ID:", resp.error || resp.message);
  } else {
    // Success — a null/empty response indicates the ID was successfully removed
    console.log("External ID removed successfully. Device is now anonymous.");
  }
});

// --- Push Notifications. External ID Management. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.

Copy the line below and paste it into your AI agent.

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Push Notifications (OneSignal) SDK: const notifications = new NativelyNotifications(); notifications.getPermissionStatus(callback) checks current permission status, returning resp.status (boolean). notifications.requestPermission(fallbackToSettings, callback) prompts the native permission dialog — fallbackToSettings: boolean, if true offers to open OS settings when previously denied — returning resp.status (boolean). notifications.getOneSignalId(callback) retrieves the device's OneSignal Player ID, returning resp.playerId (also labeled Subscription ID in the OneSignal dashboard). notifications.getExternalId(callback) and notifications.setExternalId({ externalId }, callback) manage a custom External ID linked to this Player ID, returning resp.externalId on success or resp.error/resp.message on failure. notifications.removeExternalId(callback) unlinks the External ID — a null/empty response indicates success, while resp.error/resp.message indicate failure. For reference: https://docs.buildnatively.com/natively-platform/features/notifications/onesignal-push-notifications
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Request push notification permission after the user completes onboarding, and link their account with an External ID on login".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

{% hint style="danger" %}
Client-side calls from the Natively OneSignal Push Notifications SDK can be tested directly in the Natively Preview app, but actually [sending notifications](#sending-notifications) should always happen server-side, via your backend - not from the client.
{% endhint %}

#### Time the permission request carefully

Avoid overwhelming users with a permission prompt the moment the app opens. Request push notification access at a high-value moment in the user journey instead — during onboarding, or right after a user opts into a feature that requires alerts (e.g., "Notify me when my order ships").

#### Sync devices to users on login

To ensure users receive notifications reliably across all their devices:

1. **Verify Player ID on login.** Retrieve the current device's Player ID and compare it against the ID stored in your database for that user.
2. **Register new devices.** If the current Player ID isn't in your database, the user is likely on a new device - request push permissions and register it.
3. **Sync via External ID.** Once registered, call `setExternalId` with your internal User ID, so OneSignal knows this device belongs to your user.
4. **Maintain your database.** Save the latest Player ID to your user record - consider storing these as a list to support multiple active devices (e.g. iPhone and iPad).

{% hint style="warning" %}
If a user grants permission during their current session, the updated status is reflected in OneSignal starting from the next app launch, not immediately.
{% endhint %}

Think of the Player ID as the device's digital address - without saving it, your server-side logic won't know where to deliver the notification.

## Sending Notifications

Once a user's Player ID or External ID is saved, you can send them a push notification from the **OneSignal dashboard**, the **OneSignal REST API**, or - if you're on **Bubble** - directly from a Backend Workflow.

{% tabs %}
{% tab title="Bubble.io Plugin" %}
{% hint style="warning" %}
To authorize push notifications from your Bubble workflows, navigate to the Plugin Settings tab and enter your OneSignal REST API Key into the onesignal\_apiKey field and the App ID into the onesignal\_appId field.
{% endhint %}

Detailed instructions on creating and managing your API keys can be found in the [OneSignal Documentation](https://documentation.onesignal.com/docs/accounts-and-keys).

<figure><img src="/files/3Y8RtVSlsWM9zZBilAn5" alt="" width="375"><figcaption></figcaption></figure>

**Requesting Permissions**

Trigger the permission request when a user explicitly opts in to receive notifications (e.g., flipping a "Enable Notifications" toggle).

* Existing Permissions: If the user has already granted access, the system popup will not reappear. Instead, the element will silently refresh the `OneSignal PlayerId` state.
* Handling Denials (Open Settings): If the Open Settings option is enabled and the user previously denied permissions, a dialog will appear. This prompt invites the user to navigate to their device settings to manually re-enable notifications for your app.

<figure><img src="/files/dJhx6lPNGu6INVu9h2pl" alt="" width="179"><figcaption></figcaption></figure>

<figure><img src="/files/32k142hDFthVmBvppNKC" alt="" width="228"><figcaption></figcaption></figure>

**Capturing the Player ID**

To send targeted notifications, you must map the device's unique identifier to your user records.

* Create a workflow for the event **Notification permission authorized**. Within this workflow, trigger the action **Get the user's OneSignal Player Id**. This ensures that as soon as a user grants access, the app actively fetches their new identifier.
* Listen for the **OneSignal Player Id updated** event. This event fires automatically once the identifier is successfully received from the OneSignal servers.
* Within the "Updated" event workflow, save the `OneSignal Player Id` state's value to the Current User in your database.

Think of the Player ID as the "digital address" for the device. Without saving this address to your database, your backend workflows will not know where to deliver the notification.

<figure><img src="/files/npU2nhkagXkt2AAayqiO" alt="" width="178"><figcaption></figcaption></figure>

<figure><img src="/files/Ce4Oy0DPTC5ATqT7z4MY" alt="" width="180"><figcaption></figcaption></figure>

**Sending a Notification**

In this scenario, we will schedule an automated push notification to be sent three days after a customer orders a product, informing them that their order is on its way.

1\. The Trigger (Client-Side) When a customer completes a purchase (e.g., `new_order_placed`), trigger a Backend Workflow to run with a 3-day delay. You must pass the user's Player ID and the Order Details to this backend workflow.

<figure><img src="/files/VlRSuMQ9FpwxvJCOcuGH" alt="" width="178"><figcaption></figcaption></figure>

2\. The Execution (Backend Workflow) Create a backend workflow (e.g., `send_delivery_update`) that uses the OneSignal - User Single PlayerId - Send Push action.

Configure the following parameters:

* Player ID: The unique identifier retrieved from your database.
* Title & Message: "Your order is on its way!"
* Redirect URL: The specific internal page link (e.g., `https://example.com/orders/123`) that the app will open when the user taps the notification. This parameter is optional. If not provided, the app will open the App URL.

<figure><img src="/files/30OdsSyah2a0QwKibILB" alt="" width="181"><figcaption></figcaption></figure>

3\. The Result: Deep Linking When the user receives and taps the notification, the app intercepts the Redirect URL and automatically opens that specific page within the app, rather than just the home screen.

<figure><img src="/files/owQJWSjp8LWyHnd7dZmD" alt="" width="180"><figcaption></figcaption></figure>

**Using OneSignal Templates**

If you have pre-defined layouts in your OneSignal dashboard, you can trigger them by providing a Template ID.

* The Override Rule: When a Template ID is provided, it takes precedence over the Title, Subtitle, Message, and Redirect URL fields. Any content entered into those individual fields will be ignored in favor of the template's settings.
* Setup: Simply paste your Template ID (found in the OneSignal Dashboard under Messages → Templates) into the designated field in the OneSignal - User Single PlayerId - Send Push action.

<figure><img src="/files/W9S3L11EQPl2MojZcRJB" alt="" width="176"><figcaption></figcaption></figure>

<figure><img src="/files/eZ2VDE9whRoSXSbRgSNp" alt="" width="188"><figcaption></figcaption></figure>

**Targeting by OneSignal Segments**

Use segments to broadcast messages to specific groups of users based on their behavior, location, or custom tags. Use the **OneSignal - Segments - Send Push** action in your backend workflows to reach large audiences simultaneously.

* Included Segments: Enter the exact names of the segments you wish to target (e.g., `Active Users` or `Free Tier`). To reach your entire audience, use the default OneSignal segment name: `Total Subscriptions`.
* Excluded Segments: (Optional) Specify segments that should *not* receive the notification. For example, you can target `All Users` but exclude `Premium Subscribers` to send a specific upgrade promotion.
* Important: Segment names must match the spelling and capitalization used in your OneSignal dashboard exactly.

<figure><img src="/files/ELi50UfrYg5kXVbMJ86B" alt="" width="177"><figcaption></figcaption></figure>
{% endtab %}

{% tab title="OneSignal Dashboard" %}
**OneSignal Dashboard** - the simplest way to send a test or one-off notification to an individual device, segment, or your entire audience. See [OneSignal's Mobile push setup](https://documentation.onesignal.com/docs/en/mobile-push-setup) guide.
{% endtab %}

{% tab title="Backend (REST API)" %}
**OneSignal REST API** - send notifications programmatically from your backend. See [OneSignal's REST API](https://documentation.onesignal.com/reference/push-notification) for the full request/response schema or its [Server SDKs](https://documentation.onesignal.com/docs/en/server-sdk-reference) for other platforms.
{% endtab %}
{% endtabs %}

## Custom Notification Sound

{% stepper %}
{% step %}

#### Create a sound file

According to these rules - if the device can't find it, or the format isn't supported, it falls back to the default system sound:

* Filename **must** be `natively.wav`.
* Recommended length under 30 seconds; keep file size small, as large files may not play on some devices.
  {% endstep %}

{% step %}

#### **Natively Dashboard Configuration**

1. Open your Natively app dashboard and navigate to **Features** > **Notifications** > **OneSignal**.
2. Under **Notification custom sound**, select **Click to upload file** and upload your audio file.
3. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}
{% endstep %}

{% step %}

#### OneSignal Dashboard Configuration

{% tabs %}
{% tab title="Android" %}

1. In your **OneSignal Dashboard**, go to **Settings** > **Push & In-App** > **Android Notification Channels**.
2. Click **Add Group**, name it, and click **Submit**.
3. Click **Add Channel**, name it.
4. Set Importance to **Urgent** or **High** (required for the sound to trigger).
5. Select **Custom** under the **Sound** section and set it exactly to `natively`.
6. **Create** the channel.
7. Click on the created channel and copy the **Channel ID**.

To trigger a custom sound from your Bubble workflows, navigate to the **Custom Sound** section of your **Send Push** action, and enter the copied **Channel ID** in the **Android Channel ID** field.

To trigger a custom sound via the **OneSignal REST API**, include the `android_channel_id` parameter in your JSON payload, set to the copied **Channel ID**.&#x20;

{% hint style="warning" %}
Because Android "locks" channel settings, you must reinstall your app on your device for the new sound settings to take effect.
{% endhint %}

<div><figure><img src="/files/31OXEmQPNLk36wvzA18A" alt=""><figcaption><p>Step 1-2</p></figcaption></figure> <figure><img src="/files/b9pszCFk52A64RtJ4pUs" alt=""><figcaption><p>Step 3</p></figcaption></figure> <figure><img src="/files/CWNUcR5fMcj3WeoLoQae" alt=""><figcaption><p>Step 4-6</p></figcaption></figure> <figure><img src="/files/ktdAyE3clQMupsuD2xcN" alt=""><figcaption><p>Step 7</p></figcaption></figure></div>

<figure><img src="/files/CK5ShcRkjEkltTgAD0XQ" alt="" width="89"><figcaption><p>Bubble example</p></figcaption></figure>
{% endtab %}

{% tab title="iOS" %}
To trigger a custom sound from your Bubble workflows, navigate to the **Custom Sound** section of your **Send Push** action, and enter `natively.wav` in the **iOS Sound** field.

To trigger a custom sound via the **OneSignal REST API**, include the `ios_sound` parameter in your JSON payload, set to `natively.wav`.

<figure><img src="/files/qfx1zL4Z9nthmocNJaEr" alt="" width="88"><figcaption><p>Bubble example</p></figcaption></figure>
{% endtab %}
{% endtabs %}

{% endstep %}
{% endstepper %}

## Troubleshooting

<details>

<summary>Notifications don't arrive at all</summary>

Go through this checklist:

* Confirm OneSignal is enabled and the App ID is entered correctly in the Natively Dashboard under **Features** > **Notifications** > **OneSignal**.
* Confirm the app has been rebuilt after enabling or changing this configuration.
* Confirm the Player ID or External ID being targeted actually matches the intended user/device - check in **Audience** > **Subscriptions** in your OneSignal dashboard.
* Check for [network issues](https://documentation.onesignal.com/docs/en/notifications-show-successful-but-are-not-being-shown#network-and-connectivity) on the receiving device.
* Confirm the app has push permission granted - check [app push permissions](https://documentation.onesignal.com/docs/en/notifications-show-successful-but-are-not-being-shown#permission-and-subscription-status).
* On Android, confirm the relevant [notification category](https://documentation.onesignal.com/docs/en/notifications-show-successful-but-are-not-being-shown#android-notification-categories-disabled) isn't disabled.
* Confirm the device isn't in [Low Power Mode](https://documentation.onesignal.com/docs/en/notifications-show-successful-but-are-not-being-shown#low-power-mode-and-battery-optimization) or [Do Not Disturb](https://documentation.onesignal.com/docs/en/notifications-show-successful-but-are-not-being-shown#do-not-disturb-and-focus-modes).

</details>

<details>

<summary>Notifications don't arrive on iOS specifically</summary>

Your APNs authentication key may need to be regenerated and re-uploaded to OneSignal - see [iOS Pre-OneSignal Configuration](#ios) to generate a new `.p8` key if needed.

</details>

<details>

<summary>Notifications don't arrive on Android specifically, but only during development</summary>

Delete the app and reinstall it. This is a development-only quirk and won't recur after release to the store.

</details>

<details>

<summary>OneSignal shows an "Invalid Request" error when uploading the Service Account JSON file</summary>

OneSignal doesn't indicate the actual cause of this error. The generated Service Account JSON file needs the following roles to be accepted by OneSignal:

* **Firebase Admin SDK Administrator Service Agent** - `roles/firebase.sdkAdminServiceAgent` .
* **Firebase App Check Admin** - `roles/firebaseappcheck.admin` .
* **Service Account Token Creator** - `roles/iam.serviceAccountTokenCreator` .

Check the [Google Cloud IAM console](https://console.cloud.google.com/iam-admin/iam) and confirm your Firebase Service Account has these roles.

</details>

<details>

<summary>Permission status doesn't match what the user actually granted on iOS</summary>

If **Enable iOS 12 direct to history** (Provisional Notifications) is enabled in your OneSignal dashboard, iOS may silently grant limited, quiet delivery to Notification Center without the user ever seeing a permission prompt. Natively's SDK treats provisional permission as **not fully granted** - matching how permission behaves on your website - so `getPermissionStatus` may report notifications as disabled even though the user is receiving them quietly in Notification Center. If you don't need this quiet/provisional behavior and want your app to always request full notification permission (Sounds, Banners, and Notification Center all together), disable **Direct to History** in the OneSignal dashboard.

</details>

<details>

<summary>Notifications don't work in the Natively Preview app</summary>

Devices running the Preview app use a pre-configured sandbox environment and won't appear as active subscriptions in your OneSignal dashboard. To test your actual OneSignal integration, generate a full build of your app instead.

</details>

<details>

<summary>Custom notification sound doesn't play</summary>

Confirm the file is named exactly `natively.wav`, is under 30 seconds, and has a reasonably small file size.&#x20;

On Android, confirm **Importance** is set to **Urgent** or **High** on the notification channel, and that you've reinstalled the app after configuring the channel.

</details>

<details>

<summary>Not working in a web browser</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build.

</details>

[^1]: Replace this placeholder


# Firebase - Push Notifications (Advanced)

Send push notifications to your users through Firebase Cloud Messaging.

## What are Firebase Push Notifications?

Natively provides built-in support for [Firebase Cloud Messaging](https://firebase.google.com/products/cloud-messaging) (FCM) to handle push notification delivery for your app. Firebase manages device tokens and topic-based targeting through its own console, and you send notifications either directly from the Firebase console or via the Firebase Cloud Messaging API.

{% hint style="warning" %}
If you don't have a specific reason to manage Firebase yourself, consider [OneSignal - Push Notifications](/natively-platform/features/notifications/onesignal-push-notifications) instead, which offers a simpler dashboard-driven setup and a much easier API for sending notifications.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* A [Firebase](https://firebase.google.com/) account.
* **iOS:**
  * An [Apple Developer](https://developer.apple.com/account) account.
  * Your iOS app is already [published](/natively-platform/app-info/ios-build) in the Natively Dashboard.
* **Android:**
  * Your Android app is already [published](/natively-platform/app-info/android-build) in the Natively Dashboard.

## Pre-Firebase Configuration (iOS only)

To send push notifications to iOS devices, Firebase requires the **Push Notifications** **App Capability** enabled on your Bundle ID and an **APNs authentication key** from your Apple Developer account.

{% stepper %}
{% step %}

#### App Capabilities Configuration

{% hint style="info" %}
App capabilities define what system-level features your app is allowed to use on iOS. Before Natively can include the **Firebase Push Notifications** feature in your build, the **Push Notifications** capability must be explicitly enabled in your Apple Developer account for your app's Bundle ID.
{% endhint %}

1. Open your [Apple Developer](https://developer.apple.com/account) account and navigate to [Certificates, IDs & Profiles > Identifiers](https://developer.apple.com/account/resources/identifiers/list).
2. Select your app's Bundle ID.
3. Scroll down the **Capabilities** list and enable **Push Notifications**.
4. Click **Save** and confirm.
   {% endstep %}

{% step %}

#### Set up APNs Authentication Key

1. Open your [Apple Developer](https://developer.apple.com/account) account and navigate to [Certificates, IDs & Profiles > Keys](https://developer.apple.com/account/resources/authkeys/list).
2. Click **+** to register a new key.
3. Enter **Key Name**.
4. Select the **Apple Push Notifications service (APNs)** checkbox and click **Configure**.
5. In the **Environment** dropdown menu, select **Sandbox & Production** and click **Save**.
6. Click **Continue**, then **Register**.
7. **Download** the generated `.p8` key.

{% hint style="warning" %}
You can only download this file once, so save it securely. If you lose it, you'll need to revoke the key and generate a new one.
{% endhint %}
{% endstep %}

{% step %}

#### Gather your credentials

Collect:

* **Key ID** - located in the row of the key you just created. Make sure it matches the downloaded `.p8` file.
* **Team ID** - located in your [Apple Developer](https://developer.apple.com/account) account under **Membership details**.

You'll need these for [Firebase iOS Platform configuration](#ios).
{% endstep %}
{% endstepper %}

## Firebase Configuration

To send push notifications through Firebase, you'll need a Firebase project and Platforms configured for your app.

{% stepper %}
{% step %}

#### Create or open your Firebase Project

1. Go to the [Firebase console](https://console.firebase.google.com/).
2. If you don’t have a project yet, click **Create a project** and complete the setup.
3. If you already have a project, **select it**.
   {% endstep %}

{% step %}

#### Confirm Firebase Cloud Messaging API v1 is enabled

While on the **Project Overview** page, select **Settings > General** from the left sidebar, then go to the **Cloud Messaging** tab.

{% hint style="success" %}
**Firebase Cloud Messaging API (V1)** is enabled by default for most projects.&#x20;
{% endhint %}

If it shows as disabled:

1. Click on the 3-dots menu.
2. Click **Manage the API in Google Cloud Console**.
3. Click **Enable** in the Google Cloud Console. Wait a few minutes for the change to reflect in Firebase.
   {% endstep %}

{% step %}

#### Configure your platform

{% tabs %}
{% tab title="Android" %}

1. While on the **Project Overview** page, click the **+ Add app** button under your project name.
2. Select **Android**.
3. In the **Register app** section:
   1. Enter the **Android package name** (Bundle ID) of the app configured in your Natively Dashboard.
   2. *(Optional)* Enter an **App nickname**; it will be used only in the Firebase Console to represent this app.
   3. Click **Register app**.
4. In the **Download and then add config file** section, click **Download google-services.json** and **save** the `google-services.json` file in a secure location.
5. Skip the SDK setup steps that follow, since Natively handles SDK integration for you, and click **Continue to console** at the end.

{% hint style="info" %}
You can access the `google-services.json` file again at any time. While on the **Project Overview** page, select **Settings > General** from the left sidebar, scroll down to **Your apps**, select your Android app, and download the file under the **SDK setup and configuration** section.
{% endhint %}
{% endtab %}

{% tab title="iOS" %}
{% stepper %}
{% step %}

### Configure iOS app

1. While on the **Project Overview** page, click the **+ Add app** button under your project name.
2. Select **iOS(Apple)**.
3. In the **Register app** section:
   1. Enter the **Apple Bundle ID** of the app configured in your Natively Dashboard.
   2. *(Optional)* Enter an **App nickname**; it will be used only in the Firebase Console to represent this app.
   3. *(Optional)* Enter an **App Store ID** (App Store App ID) of the app configured in your Natively Dashboard.
   4. Click **Register app**.
4. In the **Download and then add config file** section, click **Download google-services.json** and **save** the `GoogleService-Info.plist` file in a secure location.
5. Skip the SDK setup steps that follow, since Natively handles SDK integration for you, and click **Continue to console** at the end.

{% hint style="info" %}
You can access the `GoogleService-Info.plist` file again at any time. While on the **Project Overview** page, select **Settings > General** from the left sidebar, scroll down to **Your apps**, select your iOS app, and download the file under the **SDK setup and configuration** section.
{% endhint %}
{% endstep %}

{% step %}

### Upload your APNs Authentication Key

1. While on the **Project Overview** page, select **Settings > General** from the left sidebar, then go to the **Cloud Messaging** tab.
2. Scroll down to **Apple app configuration** and select your iOS app.
3. Under **APNs Authentication Key** in both **development** and **production** fields, provide the following and click **Upload**:
   * **APNs Auth key** - `.p8` file you downloaded in the [Set up APNs Authentication Key](#set-up-apns-authentication-key) step above.
   * **Key ID** and **Team ID** - gathered in the [Gather your credentials](#gather-your-credentials) step above.
     {% endstep %}
     {% endstepper %}
     {% endtab %}
     {% endtabs %}
     {% endstep %}
     {% endstepper %}

## Natively Dashboard Setup

Before proceeding, make sure you have completed the [Firebase Configuration](#firebase-configuration) steps above.

1. Open your Natively app dashboard and navigate to **Features** > **Notifications** > **Firebase**.
2. Toggle the feature to **Enabled**.
3. Upload the **Android Config file** (`google-services.json`) or/and the **iOS Config file** (`GoogleService-Info.plist`).
4. Enter the **Permission Description**.
5. *(Optional)* Adjust the **Automatically request push permission on app launch** checkbox.
6. Click **Save**.
7. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to users when the OS asks them to grant **notification** access. Explain clearly why your app needs this permission - for example: "We'll send you push notifications when your order ships."
{% endhint %}

{% hint style="info" %}
The **Automatically request push permission on app launch** is enabled by default, triggering the system permission prompt immediately after the user launches the app. Disable it if you'd rather request permission later, at a more relevant moment in your app's flow.
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### &#x20;Setup logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Push Notifications (Firebase)

{% hint style="warning" %}
This element must be set to **Visible on page load** to initialize correctly. It should be placed directly on the page root and not inside hidden containers, such as Popups, Floating Groups, Group Focus elements, or Repeating Groups. To hide the element from your UI, you may set its dimensions to 0x0 px.
{% endhint %}

{% hint style="info" %}
On initialization, the element automatically attempts to retrieve the current notification permission status - no need to call it on the **Page Is Loaded** event.
{% endhint %}

#### Events:

* **Firebase FMC Token Updated** - fires when the FCM token is updated.
* **Firebase APNS Token Updated** - fires when the APNs token is updated.
* **Notification permissions authorized** - fires when the user taps **Allow** on the permission alert.
* **Notification permissions denied** - fires when the user taps **Decline** on the permission alert.
* **Notification permissions status updated** - fires when permission status changes due to a user action.
* **Topic subscription status** - fires when a subscribe/unsubscribe action's status updates.

#### States:

* **Permission Status** - Yes/No.
* **Firebase FCM Token** -  the FCM token value.
* **Firebase APNS Token -** the APNs token value.
* **Topic subscription status** - `SUCCESS` if the subscribe action succeeded.
* **Topic unsubscribe status** - `SUCCESS` if the unsubscribe action succeeded.

#### Actions:

* **Request the user's push notification permission** - displays the system permission popup using your configured Permission Description.
* **Get the user's Firebase APNS Token** - reloads the user's Firebase APNS token.
* **Get the user's Firebase FCM Token** - reloads the user's Firebase FCM token.
* **Get the user's push notification permission status**
* **Subscribe to topic** - subscribes to an FCM topic:
  * **Topic ID** - the FCM topic name.
* **Unsubscribe from topic** - unsubscribes from an FCM topic:
  * **Topic ID** - the FCM topic name.
    {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY NOTIFICATIONS (FIREBASE) - DOCUMENTATION & EXAMPLES
// ============================================================================

const notifications = new NativelyFirebaseNotifications();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// notifications.firebase_get_token(callback)
//   - Retrieves the device's FCM token.
//
// notifications.firebase_get_apns_token(callback)
//   - Retrieves the device's APNs token. iOS only.
//
// notifications.firebase_request_permission(callback)
//   - Prompts the native OS permission dialog.
//
// notifications.firebase_has_permission(callback)
//   - Checks the current notification permission status.
//
// notifications.firebase_subscribe_to_topic(topic, callback)
//   - Subscribes the device to an FCM topic.
//   - topic: string — the FCM topic name
//
// notifications.firebase_unsubscribe_from_topic(topic, callback)
//   - Unsubscribes the device from an FCM topic.
//   - topic: string — the FCM topic name

// ============================================================================
// CALLBACK RESPONSE FIELDS - firebase_get_token
// ============================================================================
// resp.token: string | null — the device's FCM token
// Note: if Firebase throws internally, there is no structured failure response.

// ============================================================================
// CALLBACK RESPONSE FIELDS - firebase_get_apns_token
// ============================================================================
// resp.token: string | null — the device's APNs token. iOS only, on success.
// resp.status: string — "FAILED" if called on Android, or if an exception occurs
// resp.message: string — "APNS token is only available on iOS" on Android,
//               or the exception message if one occurred on iOS

// ============================================================================
// CALLBACK RESPONSE FIELDS - firebase_request_permission
// ============================================================================
// resp.status: boolean — true only if permission is fully authorized.
//              Any other permission state (denied, provisional, etc.) returns false.

// ============================================================================
// CALLBACK RESPONSE FIELDS - firebase_has_permission
// ============================================================================
// resp.status: boolean — true if notification permission is currently granted

// ============================================================================
// CALLBACK RESPONSE FIELDS - firebase_subscribe_to_topic
// ============================================================================
// resp.status: "SUCCESS" | "FAILED"
// resp.message: string — present on failure, e.g. "Exception: Topic is required"
//               if the topic parameter is missing. Other Firebase exceptions
//               also return their message here.
// Note: if the topic parameter is missing or has the wrong type entirely,
// there may be no structured failure response at all.

// ============================================================================
// CALLBACK RESPONSE FIELDS - firebase_unsubscribe_from_topic
// ============================================================================
// resp.status: "SUCCESS" | "FAILED"
// resp.message: string — present on failure, containing the Firebase exception message
// Note: if the topic parameter is missing or invalid, there may be no
// structured failure response at all.

// --- Push Notifications (Firebase). Tokens & Permission. Start ---

const fcm_token_callback = function (resp) {
  console.log(resp.token); // string, or null if no token is available
};

const apns_token_callback = function (resp) {
  if (resp.status === "FAILED") {
    console.log(resp.message); // "APNS token is only available on iOS" on Android
    return;
  }
  console.log(resp.token); // string, or null — iOS only
};

const permission_callback = function (resp) {
  console.log(resp.status); // true only if fully authorized
};

const permission_status_callback = function (resp) {
  console.log(resp.status); // true if notification permission is currently granted
};

notifications.firebase_get_token(fcm_token_callback);
notifications.firebase_get_apns_token(apns_token_callback); // iOS only
notifications.firebase_request_permission(permission_callback);
notifications.firebase_has_permission(permission_status_callback);

// --- Push Notifications (Firebase). Tokens & Permission. End ---


// --- Push Notifications (Firebase). Topic Subscription. Start ---

const topic = "your_topic_name";

const subscribe_callback = function (resp) {
  if (resp.status === "FAILED") {
    console.log(resp.message); // e.g. "Exception: Topic is required"
    return;
  }
  console.log("Subscribed to topic:", topic);
};

const unsubscribe_callback = function (resp) {
  if (resp.status === "FAILED") {
    console.log(resp.message);
    return;
  }
  console.log("Unsubscribed from topic:", topic);
};

notifications.firebase_subscribe_to_topic(topic, subscribe_callback);
notifications.firebase_unsubscribe_from_topic(topic, unsubscribe_callback);

// --- Push Notifications (Firebase). Topic Subscription. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.

Copy the line below and paste it into your AI agent.

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Push Notifications (Firebase) SDK: const notifications = new NativelyFirebaseNotifications(); notifications.firebase_get_token(callback) retrieves the device's FCM token, returning resp.token (string or null). notifications.firebase_get_apns_token(callback) retrieves the device's APNs token, iOS only, returning resp.token on success, or resp.status ("FAILED") and resp.message if called on Android or if an error occurs. notifications.firebase_request_permission(callback) prompts the native permission dialog, returning resp.status (boolean, true only if fully authorized). notifications.firebase_has_permission(callback) checks current permission status, returning resp.status (boolean). notifications.firebase_subscribe_to_topic(topic, callback) and notifications.firebase_unsubscribe_from_topic(topic, callback) manage FCM topic subscriptions, returning resp.status ("SUCCESS" or "FAILED") and resp.message on failure. For reference: https://docs.buildnatively.com/natively-platform/features/notifications/firebase-push-notifications-advanced
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Request push notification permission and save the FCM token to the user's profile on login".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Save both tokens on login - you'll need them to send notifications later

Store the FCM token (e.g. an `fcm_token` field on your user record) to send notifications on any platform. For iOS specifically, also store the APNs token (e.g. an `apns_token` field) if you send notifications directly via APNs rather than through FCM. Since tokens can change over time - after reinstalls or token refresh - retrieve and update them on every login rather than assuming a value saved once stays valid.

#### Check `resp.status` before treating permission as granted

`firebase_request_permission` only returns `true` when the user grants full authorization - any other state (denied, provisional, or not yet decided) returns `false`. Don't assume a non-true response means the request failed; it may simply mean the user hasn't fully authorized notifications.

#### Only call `firebase_get_apns_token` on iOS

This method always fails on Android with `resp.message: "APNS token is only available on iOS"`. Check the platform first with [Browser Info](/guides/integration/browser-info), and act upon the result.

#### Always provide a topic name

`firebase_subscribe_to_topic` and `firebase_unsubscribe_from_topic` fail with `"Exception: Topic is required"` if the topic is missing - validate the topic string before calling either method.

#### Use topics for broadcast-style targeting

Topics are best suited for sending the same notification to many users at once based on a shared interest or category (e.g. `news_updates`, `weekly_digest`) - subscribe users to relevant topics, then send a single notification to the topic instead of targeting each device individually.

## Sending Notifications

Once a device's FCM token (and APNs token, for iOS) is saved, you can send notifications directly through the **Firebase Console** or the **Cloud Messaging REST API**.

{% tabs %}
{% tab title="Firebase Console" %}
**Firebase Console** - the simplest way to send a test or one-off notification to either an individual device or a topic. See [Firebase Console Docs](https://firebase.google.com/docs/cloud-messaging/android/send-with-console).
{% endtab %}

{% tab title="Backend (REST API)" %}
**Cloud Messaging REST API** - send notifications programmatically from your backend to either an individual device or a topic. See [Cloud Messaging REST API Docs](https://firebase.google.com/docs/reference/fcm/rest).

{% hint style="info" %}
`YOUR_API_KEY` in the examples below is not a static API key - the FCM v1 API requires a short-lived OAuth2 access token, generated from your Firebase project's service account credentials. See [Authorizing send requests](https://firebase.google.com/docs/cloud-messaging/auth-server) for how to generate one.
{% endhint %}

{% hint style="info" %}
The `data.url` field is what enables deep linking - when the user taps the notification, Natively opens this URL inside your app instead of just launching to the home screen.
{% endhint %}

#### To a topic's subscribers:

```http
POST https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
  "message": {
    "notification": {
      "title": "Notification to All App Users",
      "body": "This is for all users of the app."
    },
    "data": {
      "url": "{YOUR_URL_TO_OPEN_INSIDE_THE_APP}"
    },
    "topic": "all"
  }
}
```

#### To a single user:

```http
POST https://fcm.googleapis.com/v1/projects/YOUR_PROJECT_ID/messages:send
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

{
  "message": {
    "notification": {
      "title": "Notification to a Single App User",
      "body": "This is for a single user of the app."
    },
    "data": {
      "url": "{YOUR_URL_TO_OPEN_INSIDE_THE_APP}"
    },
    "token": "{FCM_TOKEN}"
  }
}
```

{% endtab %}
{% endtabs %}

## Troubleshooting

<details>

<summary>Notifications don't arrive at all</summary>

Go through this checklist:

* Feature is enabled, and the config file for each platform your app supports is uploaded - `google-services.json` for Android, `GoogleService-Info.plist` for iOS. If you support both platforms, make sure both files are uploaded, not just one.
* App rebuilt after configuration. Changes to this feature only take effect after a rebuild. If you configured or updated this after your last build, rebuild and test again.
* Token actually saved and targeted correctly. Confirm the FCM token used in your send request matches a token retrieved from the device you're testing on - a stale or mistyped token will silently fail to deliver.
* Permission granted on the device. Confirm `firebase_has_permission` returns `true` on the test device - if notification permission was never granted, no notification will be delivered regardless of a correct setup.
* Testing on a real build, not Preview or a browser. This is a native feature and requires a full Natively build installed on a real device.

</details>

<details>

<summary>Notifications only appear while the app is open, not in the background or when closed</summary>

This is most commonly caused by:

* **Android missing Notifications permission.** Confirm `firebase_has_permission` returns `true` - if the runtime notification permission was never granted, foreground notifications may still work (since the app can display them directly), but system-level background notifications will be silently dropped.
* **Manufacturer battery management.** Some Android manufacturers (Xiaomi, Huawei, Samsung, and others) aggressively kill background processes or restrict notification delivery for apps not explicitly exempted from battery optimization. Check the device's battery/power management settings and allow the app to run unrestricted in the background.
* **Payload structure.** If your payload only contains a `data` block with no `notification` block, some Android OEMs won't display anything unless the app is in the foreground to handle it manually - see see [Firebase Cloud Messaging message types](https://firebase.google.com/docs/cloud-messaging/customize-messages/set-message-type) for the distinction.

</details>

<details>

<summary>REST API call fails or returns an error</summary>

Confirm you're using a valid, current OAuth2 access token as the Bearer token - not a static Server Key, which is only valid for the deprecated legacy HTTP API. See the [FCM error codes reference](https://firebase.google.com/docs/reference/fcm/rest/v1/ErrorCode) to interpret the specific error returned.

</details>

<details>

<summary>Notification doesn't trigger any app behavior when tapped, or doesn't deep link correctly</summary>

Confirm the `data.url` field is included in your payload with the correct URL. If your payload only includes a `notification` block with no `data` block, the OS displays the notification but your app has nothing to act on when it's tapped - see [Firebase Cloud Messaging message types](https://firebase.google.com/docs/cloud-messaging/customize-messages/set-message-type) for the distinction.

</details>

<details>

<summary>Notifications sent from the Firebase Console arrive with a noticeable delay</summary>

This is expected behavior for messages sent as **Campaigns** through the Firebase Console UI - Firebase batches and schedules campaign delivery rather than sending immediately, which can introduce a delay of several minutes or more. For immediate, real-time delivery, send via the **Cloud Messaging HTTP v1 API** instead.

</details>

<details>

<summary>Not working in a web browser or in the Natively Preview app</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build.

</details>

[^1]: Replace this placeholder


# Photo Library

Grant your app persistent access to the user's entire photo library.

## What is Photo Library?

The Photo Library feature gives your app broad, persistent access to the user's entire photo library on both iOS and Android - including the Android `READ_MEDIA_IMAGES` and `READ_MEDIA_VIDEO` permissions, and the iOS `NSPhotoLibraryUsageDescription` (Photos) permission.&#x20;

{% hint style="danger" %}
Enable it only if your app has a core function that requires this - a **photo editor** or **gallery management** app, for example (e.g. Instagram).&#x20;

Google Play has been rejecting apps that request these permissions without this kind of core use case - including marketplace or classifieds apps that only need users to upload a few photos.
{% endhint %}

The device's standard system picker is always available to your app regardless of this setting. If you only need to let users upload a photo - a profile picture, a listing image, an attachment - **you don't need to enable this feature at all**; the system picker handles that without requiring broad permissions.

## Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **Photo Library**.
2. Toggle the feature to **Enabled**.
3. Select which Android permissions your app needs: `READ_MEDIA_IMAGES`, `READ_MEDIA_VIDEO`, or both - only check the ones your app actually uses.
4. Enter the **Permission Description**.
5. Click **Save**.
6. Rebuild your app(s).

{% hint style="info" %}
The **Permission Description** is the text shown to users when the OS asks them to grant **photo library** access. Explain clearly why your app needs this permission - for example: "We use your photo library to let you browse and select multiple images for your gallery".
{% endhint %}

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## How to use

#### Default to the system picker

For most apps - profile pictures, listing photos, message attachments - the standard file input picker covers it without needing this feature enabled at all. Only enable Photo Library if your app's core purpose depends on browsing or managing the user's full photo library.

#### Enable it for gallery and editing apps

If your app lets users browse their camera roll, select multiple photos at once, or manage albums, Photo Library is the right choice - the system picker isn't built for that kind of bulk or repeated access.

#### Only check the Android permissions your app actually uses

If your app only works with photos and never handles video, check `READ_MEDIA_IMAGES` alone rather than both - requesting permissions you don't use increases rejection risk.

#### Write a Permission Description that matches the actual use case

Since this permission grants broad access on both iOS and Android, be specific about why - vague descriptions like "to improve your experience" are a common cause of rejection. Explain the real feature, e.g. "We use your photo library to let you browse and select multiple images for your gallery".

## Troubleshooting

<details>

<summary>App rejected by Google Play Store or Apple App Store</summary>

Both stores may reject apps where the underlying use case doesn't justify persistent, full-library access - marketplace, classifieds, and listing apps are common examples that get rejected even with a valid description. Only enable this feature for apps like photo editors or gallery managers where broad access is core to the app's function; otherwise, use the system picker (default behavior).

</details>


# Service Worker

Cache resources locally on the device to speed up loading and enable offline access.

## What is Service Worker?

Service Worker Support lets your app cache specific resources and data directly on the user's device. Once configured, previously loaded content is served from the local cache instead of the network, which speeds up app loading and allows parts of your app to remain usable even when the device is offline.

{% hint style="danger" %}
Natively provides the environment for the Service Worker to run inside the mobile app - the caching logic itself and the `service-worker.js` file must already be implemented and active on your website. We recommend the official [MDN Web Docs: Service Worker API](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) or [Google's Workbox Guide](https://developer.chrome.com/docs/workbox/) for best practices on building one.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* A working Service Worker (`service-worker.js`) already implemented and active on your website.

## Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **Service Worker**.
2. Toggle the feature to **Enabled**.
3. Under **Domains**, add the domains the Service Worker is allowed to cache resources from (e.g. `example.com`, `cdn.example.com`). You can add up to **10 hosts**.
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## How to use

#### Enabling this feature disables the default Network Check

When Service Worker is enabled, Natively's default network connectivity check is automatically disabled. Your website is now responsible for its own fallback UI for when there's no network connection and no cached data available to serve.

#### Disable Continual Network Check if you want true offline access

If your goal is to let users access the app while offline, you must also disable [**Continual Network Check**](/natively-platform/appearance/network-screen#continual-network-check-feature) under your [Error Screen](/natively-platform/appearance/network-screen) settings. If left enabled, Natively will show the native Error Screen as soon as the device goes offline - before your cached content ever gets a chance to load - blocking the exact experience the Service Worker is meant to provide.

#### Only whitelisted hosts are cached

Resources are only cached from domains listed under the Service Worker feature **Domains** configuration. If your app loads assets from a CDN or a separate subdomain, make sure to add each one - anything not listed will always be fetched from the network.

#### For iOS, list every domain your app might redirect to - not just your own

On iOS, Service Worker restricts navigation to domains listed under **Domains** - any redirect to a domain not on that list will be blocked by the OS. This includes third-party domains you don't own but redirect to as part of a normal flow, such as a payment provider.

## Troubleshooting

<details>

<summary>Resources aren't being cached</summary>

Confirm the resource's domain is listed under **Trusted Hosts** in the Natively Dashboard, and that your website's `service-worker.js` is actually registering and caching that resource - Natively only provides the runtime, not the caching logic itself.

</details>

<details>

<summary>The app shows the Error Screen immediately when offline, even though the Service Worker is enabled</summary>

Confirm that [**Continual Network Check**](/natively-platform/appearance/network-screen#continual-network-check-feature) is disabled under your [Error Screen](/natively-platform/appearance/network-screen) settings - it overrides cached content by forcing the Error Screen as soon as connectivity drops.

</details>

<details>

<summary>App behaves differently than expected after going offline</summary>

Since your website's fallback UI is now responsible for the no-network, no-cache case (the default Network Check is disabled once the Service Worker is on), verify that the fallback UI is implemented and tested on your website directly.

</details>


# Social Auth

Enable Google, Facebook, Microsoft, KakaoTalk, or any other OAuth provider login directly inside your Natively app.

## What is Social Auth?

Social Auth enables third-party OAuth login inside your Natively app. Instead of building a custom authentication system, your users can sign in with an account they already have - Google, Facebook, Microsoft, KakaoTalk, or any other OAuth provider.

{% hint style="warning" %}
If your app offers any third-party login options, Apple requires you to also offer [Sign In with Apple](/natively-platform/features/social-auth/sign-in-with-apple) for your iOS app.
{% endhint %}

## How does it work?

When a user taps a social login button in your app, Natively intercepts the OAuth request and handles it outside the normal embedded browser flow. The exact behavior depends on the platform, provider, and OS version.

### iOS

Natively opens Apple's secure native authentication sheet ([ASWebAuthenticationSession](https://developer.apple.com/documentation/authenticationservices/aswebauthenticationsession)) to handle the OAuth flow. Before the sign-in page loads, iOS shows a system dialog asking the user to confirm they want to continue. Once confirmed, the sign-in page loads inside the sheet - the user signs in without fully leaving the app. Once authentication is complete, the provider [redirects](#redirect-url) back to your app to finish the sign-in.

{% hint style="danger" %}
For Social Auth to work correctly on iOS, your [AASA](/natively-platform/features/deep-links/universal-links#apple-app-site-association) file must include the `webcredentials` section. Without it, the native authentication sheet will not be triggered, and the authentication will fall back to the internal browser instead.
{% endhint %}

{% hint style="info" %}
Facebook on iOS is an exception - instead of the native session, Natively shows a notice asking the user to open the login page in an external browser. Cancelling reloads the current page. Confirming opens Facebook login externally, returning the user via [Deep Links](/natively-platform/features/deep-links) after authentication.
{% endhint %}

{% hint style="info" %}
If authentication fails or the user cancels it on iOS, Natively reloads the current page. It is not possible to intercept or handle these events in your web app.
{% endhint %}

### Android

Natively shows a Redirect notice asking the user to open the login page in the default browser. Cancelling reloads the current page. Confirming opens the login page in the device's default browser. Once authentication is complete, the provider [redirects](#redirect-url) back to your app to finish the sign-in.

{% hint style="info" %}
On Android 10 and older, the login page opens directly inside the embedded browser instead of the default browser. No dialog is shown. Natively adjusts the browser identification so the OAuth provider treats it as a regular mobile browser.
{% endhint %}

## Prerequisites&#x20;

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* [Deep Links](/natively-platform/features/deep-links) configured and working on both iOS and Android - required for the device to route the OAuth redirect back to your app.
* Your OAuth provider configured with the correct [Redirect URL](#redirect-url) before setting up Social Auth in the Natively Dashboard.

## Natively Dashboard Setup

{% hint style="danger" %}
[Deep Links](/natively-platform/features/deep-links) must be enabled and functioning correctly before setting up Social Auth. Without them, users cannot be redirected back to your app after authentication.
{% endhint %}

1. Open your Natively app dashboard and navigate to **Features** > **Social Auth**.
2. Toggle the feature to **Enabled**.
3. Enter your **Redirect URL** -  see [Redirect URL](#redirect-url) below for details.
4. *(Optional)* Add **Custom OAuth URLs** - see [Custom OAuth URLs](#custom-oauth-urls) below for details.
5. Click **Save**.
6. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

### Redirect URL

The Redirect URL tells Natively which URL to watch for to know that authentication is complete. Once the OAuth provider finishes authenticating the user, it redirects to this URL carrying the authentication token. Natively detects this match, intercepts the redirect, and loads that final URL back into your mobile app, completing the sign-in.

{% hint style="warning" %}
The Redirect URL must exactly match the redirect URL configured on your OAuth provider's side (e.g. in Google Cloud Console). If they don't match, the provider will not redirect to the correct URL, and the authentication will fail.
{% endhint %}

{% hint style="warning" %}
The Redirect URL must be on the same domain as your [Deep Links](/natively-platform/features/deep-links) configuration. Deep Links are what allow the device to route the redirect back to your app instead of opening it as a normal web page. If the domains don't match, the user will not be redirected back to your app after authentication.
{% endhint %}

### Custom OAuth URLs

By default, Natively recognizes a set of built-in OAuth provider URLs: Google, Facebook, Microsoft, and KakaoTalk, and applies the native authentication flow to them automatically. Custom OAuth URLs allow you to expand this list with any additional OAuth provider your app uses.

When Natively detects a login URL that matches one of your custom entries, it applies the same authentication flow as for the built-in providers: native authentication sheet on iOS and an external browser on Android.

{% hint style="danger" %}
Do not add your own website or app domain to Custom OAuth URLs. Any link from your app to your own domain that matches a custom OAuth URL will trigger the redirect notice on Android or open a native authentication sheet on iOS instead of loading normally inside the app. Only add URLs that belong to the OAuth provider's authentication endpoints.
{% endhint %}

{% hint style="warning" %}
Custom OAuth URLs must include both a host and a path - for example `accounts.yourprovider.com/o/oauth2`. A domain alone without a path is not valid.
{% endhint %}

{% hint style="info" %}
Natively cannot verify whether a custom OAuth URL will work correctly with a given provider. Built-in providers like Google and Microsoft have been tested and verified. Custom providers should be tested thoroughly before releasing to users.
{% endhint %}

## Supported providers

Natively has built-in support for Google, Facebook, Microsoft, and KakaoTalk.

Any other OAuth provider can be added via [Custom OAuth URLs](#custom-oauth-urls). Note that unlisted providers are not guaranteed to work and should be tested thoroughly before releasing to users.

## Troubleshooting

<details>

<summary>Native authentication sheet not triggering on iOS, falls back to internal browser</summary>

Your AASA file is missing the `webcredentials` section, or it's not correctly formatted. Verify your file using the [Universal Links verification tools](/natively-platform/features/deep-links/universal-links#how-to-verify).

</details>

<details>

<summary>User is not redirected back to the app after authentication</summary>

Check the following:

* Redirect URL in Natively Dashboard matches the redirect URL configured on your OAuth provider's side exactly.
* Redirect URL domain matches your Universal Links Associated Domain.
* Deep Links are correctly configured and verified.
* App has been rebuilt after enabling Social Auth.

</details>

<details>

<summary>User stuck in browser, never returns to app</summary>

Most likely caused by a Redirect URL mismatch between Natively Dashboard and your OAuth provider configuration, or Universal Links not functioning correctly on the device.

</details>

<details>

<summary>User completes login but is not authenticated, or lands on the wrong page</summary>

On Android, the app may open even if the Redirect URL doesn't exactly match what Natively is watching for - Android Universal Links can still route the user back to the app, but the final URL loaded inside the app may not be the correct callback page, resulting in a failed or incomplete authentication.&#x20;

On iOS, this wouldn't happen - if the Redirect URL doesn't match, the app won't open at all.&#x20;

Verify that the Redirect URL in the Natively Dashboard exactly matches the callback URL configured in your OAuth provider and your web app.

</details>

<details>

<summary>Custom OAuth provider not working</summary>

Natively cannot guarantee untested providers. Verify your Custom OAuth URL includes both host and path, and confirm the provider's login flow doesn't significantly differ from standard OAuth redirect behavior.

</details>

<details>

<summary>Tapping a normal in-app link unexpectedly opens the native auth sheet or browser notice</summary>

A Custom OAuth URL includes a URL that matches your own website or app domain. Remove your own domain from Custom OAuth URLs - only OAuth provider authentication endpoints should be listed there.

</details>

<details>

<summary>Social Auth works after publishing, but fails in testing</summary>

Universal Links and Android App Links can be unreliable in development/testing builds, depending on how the app is installed and signed. They are expected to work reliably once the app is properly published to the App Store / Play Store. If you're seeing failures only in testing, verify your AASA file and assetlinks.json are correctly hosted and that the test build matches your production signing configuration.

</details>

<details>

<summary>Microsoft login fails for company/workspace accounts</summary>

Some Microsoft Workspace accounts have security settings that block login inside a WebView-based flow. The account administrator may need to adjust these settings.

</details>


# Google OAuth

Set up Google OAuth to enable Sign in with Google in your mobile app.

This guide walks you through configuring Natively [Social Auth](/natively-platform/features/social-auth) so that Sign in with Google works correctly inside your Natively app. It covers the general setup process using your own Google Cloud Console project, as well as platform-specific instructions for Lovable, Base44, and Replit.

## Prerequisites

* [Universal Links](/natively-platform/features/deep-links/universal-links) configured and working on both iOS and Android.
* [Google Cloud account](https://console.cloud.google.com) - only required if you're using your own Google Cloud OAuth credentials, or building a custom implementation.

{% hint style="warning" %}
If you're building with an AI-powered editor like Lovable, Base44, or Replit, skip ahead to [AI Agents Platform Setup](#ai-agents-platform-setup).
{% endhint %}

## General Setup

This section covers setting up Google OAuth from scratch using your own Google Cloud project - applicable to Bubble, custom implementations, or any platform not covered in the AI Agents Platform Setup section below.

{% hint style="warning" %}
This guide covers only the configuration of the Google Cloud Console and Natively Dashboard. Implementing the actual Sign in with Google button and logic on your website is your responsibility - refer to [Google's official OAuth documentation](https://developers.google.com/identity/protocols/oauth2) for that part.
{% endhint %}

### Google Cloud Console Configuration

#### Create a Google Cloud Project

If you don't already have a Google Cloud project, create one first:

1. Open [Google Cloud Console](https://console.cloud.google.com)
2. Click the project selector at the top and select **New Project**.
3. Enter a project name and click **Create**.

#### Configure OAuth Consent Screen

Before creating credentials, you need to configure the OAuth consent screen - this is what users see when they are asked to grant your app access to their Google account.

1. In the Google Cloud Console, navigate to **APIs & Services** > **OAuth consent screen**. In the opened window, click **Get Started**.
2. Fill in the required fields:
   1. **App name** - the name users will see on the consent screen.
   2. **User support email** - an email users can contact for support.
   3. **Audience** - select **External** as the user type.
   4. **Developer contact information** - your email address.
3. Click **Save and Continue** through the remaining steps.

#### Create OAuth Credentials

1. Navigate to the [Clients page](https://console.developers.google.com/auth/clients).
2. Click **Create Client**.
3. Select **Web application** as the application type.
4. Enter a name for your OAuth client.
5. *(Optional)* Under **Authorized JavaScript origins**, add your app's domain (e.g. `https://yourdomain.com`) - only required if your implementation makes OAuth requests directly from client-side JavaScript.
6. Under **Authorized redirect URIs**, add your app's redirect URL - see [Redirect URL](#redirect-url) below for the correct format.
7. Click **Create**.
8. Copy your **Client ID** and **Client Secret** - you will need these in your platform setup.

{% hint style="warning" %}
The **Client Secret** is only shown once at creation time, and the JSON file is your only way to retrieve it later. Download and store it securely before closing the dialog.
{% endhint %}

### Redirect URL

The Redirect URL is the URL your web app uses to handle the authentication token after the user successfully signs in with Google. It must match exactly what you configure in your Google Cloud OAuth client's Authorized redirect URLs - for example `https://yourdomain.com/auth/callback`.

{% hint style="warning" %}
Google requires an exact match between the **Redirect URL** and the **Authorized redirect URI** in Google Cloud Console, including the scheme (`https`), trailing slashes, and path. Even a small mismatch will cause a `redirect_uri_mismatch` error from Google.
{% endhint %}

{% hint style="warning" %}
The Redirect URL must be on the same domain as your Universal Links configuration. If the domains don't match, the user will not be redirected back to your app after authentication.
{% endhint %}

### Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **Social Auth**.
2. Enter your **Redirect URL** - the same value you added to **Authorized redirect URIs** in [Google Cloud Console](#create-oauth-credentials).
3. Click **Save**.
4. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

{% hint style="info" %}
No **Custom OAuth URL** is needed for the general setup - Google is a built-in OAuth provider already recognized by Natively.
{% endhint %}

## AI Agents Platform Setup

{% hint style="warning" %}
This section documents how Google OAuth is currently configured on Base44, Lovable, and Replit, as of the time of writing. These platforms are not controlled by Natively, and their setup flows may change at any time. If something in this guide no longer matches what you see on your platform, check that platform's own documentation or support for the most current steps.
{% endhint %}

{% hint style="warning" %}
The steps below reflect the configuration that worked in our own test apps for each platform. Your app's authentication setup may differ - for example, if you've customized the login flow, added extra redirect logic, or modified how callbacks are handled. We recommend validating each redirect and callback in your own app before relying on this configuration, since we don't manage or know about your specific implementation.
{% endhint %}

{% tabs %}
{% tab title="Lovable" %}
Lovable offers two ways to set up Google sign-in: [**Managed by Lovable**](#managed-by-lovable), where Lovable handles the OAuth credentials for you, or [**Your own credentials**](#your-own-credentials), where you connect your own Google Cloud OAuth client. Both result in the same sign-in experience for your users - the difference is who manages the credentials and what branding appears on the consent screen.

### Managed by Lovable

This is the default and simplest option - no Google Cloud Console setup required.

{% stepper %}
{% step %}

#### Set up and test Google Sign-In on your website

Before connecting Google OAuth to Natively, make sure Google sign-in works correctly on your website first. Follow [Lovable's official guide](https://docs.lovable.dev/features/google-auth) to enable Google authentication. Test the flow on your live website - sign in and sign out - to confirm it works before continuing.
{% endstep %}

{% step %}

#### Set a custom Redirect URL (callback)

By default, Lovable redirects users back to your app's root URL after sign-in. For Natively to correctly detect when authentication is complete, you need a dedicated callback URL.

Ask Lovable:

```
Set my Google sign-in to use a custom redirect URL of window.location.origin + '/auth/callback' and create a matching /auth/callback page that finalizes the session and navigates to /
```

{% hint style="info" %}
`/auth/callback` is just an example path. You can use any path you'd like, as long as it matches exactly between your Lovable app and the Redirect URL you configure in the Natively Dashboard in the next step.
{% endhint %}

Test again on your live website to confirm sign-in still works correctly with the new callback.
{% endstep %}

{% step %}

#### Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **Social Auth**.
2. Enter your **Redirect URL** - must exactly match the callback path you configured in Step 2.
3. Add to **Custom OAuth URLs**:  `oauth.lovable.app/initiate` .
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

{% hint style="danger" %}
The **Custom OAuth URL** must be entered exactly as provided. Any difference in the path will prevent Natively from correctly detecting Lovable's Google sign-in flow.
{% endhint %}
{% endstep %}
{% endstepper %}

### Your own credentials

Use this option if you need full control over the consent screen branding or have specific compliance requirements. Requires a [Google Cloud Console configuration](#google-cloud-console-configuration).

{% stepper %}
{% step %}

#### Google Cloud OAuth configuration

1. Set up your [Google Cloud OAuth credentials](#create-oauth-credentials).&#x20;
2. The Authorized Redirect URI to add in Google Cloud Console is `https://oauth.lovable.app/callback`.
   {% endstep %}

{% step %}

#### Lovable project configuration

In your Lovable project:

1. Go to **More** > **Cloud** > **Users** > **Auth** > **Google**.
2. Select **Your own credentials**.
3. Enter your **Client ID** and **Client secret** from Google Cloud Console.
4. Under **Redirect URL(s)**, check **only** `https://oauth.lovable.app/callback` , leave `https://{yourdomain}/~oauth/callback` **unchecked**.
5. **Save** the changes.
   {% endstep %}

{% step %}

#### Natively Dashboard setup

Follow the same Steps 1-3 from the [Managed by Lovable](#managed-by-lovable) section to set up and test Google Sign-In, set a custom [Redirect URL](#set-a-custom-redirect-url-callback), and configure the [Natively Dashboard](#natively-dashboard-setup-1).&#x20;
{% endstep %}
{% endstepper %}

{% hint style="danger" %}
Selecting **both** checkboxes, or selecting **only** your app's own domain, will cause authentication to fail. **Only** `https://oauth.lovable.app/callback` should be checked.
{% endhint %}

### Testing

After completing the setup above, you need to test Google Sign-In on a real device. If sign-in doesn't work as expected, see the [Troubleshooting](#troubleshooting) section below, or the [Social Auth Troubleshooting](/natively-platform/features/social-auth#troubleshooting) section for issues related to the overall sign-in flow.
{% endtab %}

{% tab title="Base44" %}
Base44 offers two ways to set up Google sign-in: [**Default Base44 OAuth**](#default-base44-oauth), which uses Base44's own credentials, or [**Custom OAuth from Google Console**](#custom-oauth-from-google-console), where you connect your own Google Cloud OAuth client. Both result in the same sign-in experience for your users - the difference is who manages the credentials and what branding appears on the consent screen.

### Default Base44 OAuth

This is the default and simplest option - no Google Cloud Console setup required.

{% stepper %}
{% step %}

#### Set up and test Google Sign-In on your website

Before connecting Google OAuth to Natively, make sure Google sign-in works correctly on your website first. Follow [Base44's official guide](https://docs.base44.com/Setting-up-your-app/Managing-login-and-registration#customizing-the-google-login) to enable Google authentication. Test the flow on your live website - sign in and sign out - to confirm it works before continuing.
{% endstep %}

{% step %}

#### Set a custom Redirect URL (callback)

By default, Base44 may redirect users back to your app's root URL after sign-in, or use an internal flow that doesn't support a custom callback. For Natively to correctly detect when authentication is complete, you need a dedicated callback URL.

Ask Base44:

```
Set my Google sign-in to use a custom redirect URL of window.location.origin + '/auth/callback' and create a matching /auth/callback page that finalizes the session and navigates to /
```

{% hint style="info" %}
`/auth/callback` is just an example path. You can use any path you'd like, as long as it matches exactly between your Base44 app and the Redirect URL you configure in the Natively Dashboard in the next step.
{% endhint %}

Test again on your live website to confirm sign-in still works correctly with the new callback.
{% endstep %}

{% step %}

#### Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **Social Auth**.
2. Enter your **Redirect URL** - must exactly match the callback path you configured in Step 2.
3. Add to **Custom OAuth URLs**:  `app.base44.com/api/apps/auth/login` .
4. Click **Save**.
5. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

{% hint style="danger" %}
The **Custom OAuth URL** must be entered exactly as provided. Any difference in the path will prevent Natively from correctly detecting Base44's Google sign-in flow.
{% endhint %}
{% endstep %}
{% endstepper %}

### Custom OAuth from Google Console

{% hint style="info" %}
Custom OAuth from Google Console is only available on Base44's **Builder** plan or higher.
{% endhint %}

Use this option if you need full control over the consent screen branding or have specific compliance requirements. Requires a [Google Cloud Console configuration](#google-cloud-console-configuration).

{% stepper %}
{% step %}

#### Google Cloud OAuth configuration

1. Set up your [Google Cloud OAuth credentials](#create-oauth-credentials).&#x20;
2. The Authorized Redirect URI to add in Google Cloud Console is `https://app.base44.com/api/apps/auth/callback`.
   {% endstep %}

{% step %}

#### Base44 project configuration

In your Base44 project:

1. Go to **Dashboard** > **Settings** > **Authentication** > **Google authentication**.
2. **Select Use a custom OAuth from Google Console.**
3. Enter your **Client ID** and **Client secret** from Google Cloud Console.
4. Click **Update** to save the changes.
   {% endstep %}

{% step %}

#### Natively Dashboard setup

Follow the same Steps 1-3 from the [Default Base44 OAuth](#default-base44-oauth) section to set up and test Google Sign-In, set a custom [Redirect URL](#set-a-custom-redirect-url-callback-1), and configure the [Natively Dashboard](#natively-dashboard-setup-3).&#x20;
{% endstep %}
{% endstepper %}

### Testing

After completing the setup above, you need to test Google Sign-In on a real device. If sign-in doesn't work as expected, see the [Troubleshooting](#troubleshooting) section below, or the [Social Auth Troubleshooting](/natively-platform/features/social-auth#troubleshooting) section for issues related to the overall sign-in flow.
{% endtab %}

{% tab title="Replit" %}
Replit uses [Clerk](https://clerk.com) to handle authentication, including Google sign-in. Replit offers two ways to set up Google sign-in: [**Replit-managed credentials**](#replit-managed-credentials), the default option, or [**Custom credentials**](#custom-credentials), where you connect your own Google Cloud OAuth client. Both result in the same sign-in experience for your users - the difference is who manages the credentials and what branding appears on the consent screen.

### Replit-managed credentials

This is the default and simplest option - no Google Cloud Console setup required.

{% stepper %}
{% step %}

#### Set up and test Google Sign-In on your website

Before connecting Google OAuth to Natively, make sure Google sign-in works correctly on your website first. Follow [Replit's official guide](https://docs.replit.com/references/auth-and-identity/google) to enable Google authentication. Test the flow on your live website - sign in and sign out - to confirm it works before continuing.
{% endstep %}

{% step %}

#### Set a custom Redirect URL (callback)

By default, Replit may redirect users back to your app's root URL after sign-in. For Natively to correctly detect when authentication is complete, you need a dedicated callback URL.

Ask Replit:

```
Set my Google sign-in to use a custom redirect URL of window.location.origin + '/auth/callback' and create a matching /auth/callback page that finalizes the session and navigates to /
```

{% hint style="info" %}
`/auth/callback` is just an example path. You can use any path you'd like, as long as it matches exactly between your Replit app and the Redirect URL you configure in the Natively Dashboard in the next step.
{% endhint %}

Test again on your live website to confirm sign-in still works correctly with the new callback.
{% endstep %}

{% step %}

#### Natively Dashboard Setup

1. Open your Natively app dashboard and navigate to **Features** > **Social Auth**.
2. Enter your **Redirect URL** - must exactly match the callback path you configured in Step 2.
3. Click **Save**.
4. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

{% hint style="info" %}
No **Custom OAuth URL** is needed for Replit - the Google sign-in flow goes directly to Google and back to your app's domain without passing through an intermediate platform URL.
{% endhint %}
{% endstep %}
{% endstepper %}

### Custom credentials

{% hint style="warning" %}
Custom Google OAuth credentials in Replit are only available in the **Production** environment, not in development.
{% endhint %}

Use this option if you need full control over the consent screen branding or have specific compliance requirements. Requires a [Google Cloud Console configuration](#google-cloud-console-configuration).

{% stepper %}
{% step %}

#### Google Cloud OAuth configuration

1. Set up your [Google Cloud OAuth credentials](#create-oauth-credentials).&#x20;
2. You will need your Replit **Redirect callback URLs** and **JavaScript origins** values for this - see Step 2 below.
   {% endstep %}

{% step %}

#### Replit project configuration

In your Replit project:

1. Go to **Auth pane** > **Configure tab** > **SSO providers** > **Google**.
2. Select **Custom credentials.**
3. Enter your **Client ID** and **Client secret** from Google Cloud Console.
4. Under **Provider setup**, copy the values shown for **Redirect callback URLs** and **JavaScript origins**, and add them to your OAuth client's **Authorized redirect URIs** and **Authorized JavaScript origins** in Google Cloud Console.
5. Click **Mark all as done**.
6. Click **Save changes**.
   {% endstep %}

{% step %}

#### Natively Dashboard setup

Follow the same Steps 1-3 from the [Replit-managed credentials](#replit-managed-credentials) section to set up and test Google Sign-In, set a custom [Redirect URL](#set-a-custom-redirect-url-callback-2), and configure the [Natively Dashboard](#natively-dashboard-setup-6).&#x20;
{% endstep %}
{% endstepper %}

### Testing

After completing the setup above, you need to test Google Sign-In on a real device. If sign-in doesn't work as expected, see the [Troubleshooting](#troubleshooting) section below, or the [Social Auth Troubleshooting](/natively-platform/features/social-auth#troubleshooting) section for issues related to the overall sign-in flow.
{% endtab %}
{% endtabs %}

## Troubleshooting

<details>

<summary><code>disallowed_useragent</code> error from Google</summary>

Google blocks OAuth requests made from embedded browsers (WebView) for security reasons. If you see this error, it means the Google sign-in request is loading inside Natively's embedded browser (Internal Browser) instead of being intercepted and opened in the native authentication flow. Check whether your platform uses any intermediate redirect before reaching Google's OAuth screen (for example, `oauth.lovable.app/initiate` or `app.base44.com/api/apps/auth/login`), and make sure that the exact URL is added to Custom OAuth URLs in the Natively Dashboard.

</details>

<details>

<summary><code>redirect_uri_mismatch</code> error from Google</summary>

This error comes directly from Google and means the redirect URI your app is sending in the OAuth request doesn't match any of the Authorized redirect URIs registered in your Google Cloud OAuth client. This is unrelated to the Natively Dashboard configuration. Check your Google Cloud Console OAuth client and make sure the exact redirect URI being used (by your platform or website) is added, including the matching scheme (`http` vs `https`), trailing slashes, and path exactly.

</details>

<details>

<summary>User is stuck in the browser after signing in and never returns to the app</summary>

This usually means the Redirect URL Natively is watching for does not match the URL the OAuth provider is actually redirecting to. Double-check that the Redirect URL in the Natively Dashboard matches exactly what's configured on your platform and in Google Cloud Console.&#x20;

This can also happen if [Universal Links](/natively-platform/features/deep-links/universal-links) are not correctly configured, meaning that the app just cannot be opened when the website domain is triggered.

</details>

<details>

<summary>Sign-in works on the website but fails in the Natively app</summary>

There are many possible causes here: Universal Links misconfiguration, an incorrect Custom OAuth URL, or a Redirect URL mismatch. Rather than checking each setting individually, the most reliable approach is to trace the actual redirect chain your website's Google sign-in flow goes through (using a tool like a network inspector or a URL tracer), and compare each step against what's configured in the Natively Dashboard - Custom OAuth URLs should match any intermediate redirect domains, and Redirect URL should match the final destination URL exactly.

</details>

<details>

<summary>Google account picker is skipped; the user is signed in automatically</summary>

If your device has only one Google account signed in, Google may skip the account picker and sign in automatically without showing the consent screen. This has been observed when using your own Google Cloud credentials on Lovable, Base44, and Replit. This happens because the `prompt` parameter is not being passed by the platform's OAuth request. This is a platform-level behavior and cannot be managed from the Natively side.

</details>

<details>

<summary>Google Sign-in is not working in the Natively Preview app</summary>

Social Auth is not available in the Natively Preview app. Test the full sign-in flow on a real device using a full build instead.

</details>

<details>

<summary>System dialog shows the platform's domain instead of Google (or another OAuth provider)</summary>

On iOS, the native authentication sheet displays a system dialog like *"'\[Your App]' Wants to Use '\[domain]' to Sign In"* before the sign-in page loads. Even though you've configured Google (or another provider) for sign-in, this dialog may show a different domain instead - for example `app.base44.com` rather than anything related to Google. This happens because platforms like Base44 and Lovable redirect through their own domain before reaching Google, and Natively's native authentication sheet reflects the first domain it intercepts, not the final OAuth provider. This is expected behavior and cannot be changed, as it depends entirely on how the platform structures its OAuth redirects.

</details>


# Sign In with Apple

Allow users to sign in to your app using their Apple ID - no password required.

## What is Sign In with Apple?

Sign In with Apple lets your users authenticate using their Apple ID with a single tap, using Face ID or Touch ID to confirm. It's a fast, privacy-friendly login option that doesn't require users to create a new account or remember a password. Because Apple can hide users' real email addresses behind a private relay, users often trust it more than other login options.

{% hint style="info" %}
Sign In with Apple feature uses a native iOS component - no [Social Auth](/natively-platform/features/social-auth) or [Deep Links](/natively-platform/features/deep-links) required.&#x20;

However, if your app uses the Social Auth feature to offer any other third-party login (Google, Facebook, etc.), Apple requires you to also include Sign In with Apple, and those OAuth providers will require Social Auth and Deep Links.&#x20;
{% endhint %}

{% hint style="warning" %}
Sign In with Apple is only available on iOS. It should not appear or function on Android devices or in a web browser.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature requires the **Unlimited** or **Lifetime** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

* An [Apple Developer](https://developer.apple.com/account) account.
* Your iOS app is already [published](/natively-platform/app-info/ios-build) in the Natively Dashboard.

## App Capabilities Configuration

{% hint style="info" %}
App capabilities define what system-level features your app is allowed to use on iOS. Before Natively can include the Sign In with Apple feature in your build, the **Sign In with Apple** capability must be explicitly enabled in your Apple Developer account for your app's Bundle ID.
{% endhint %}

* Open your [Apple Developer account](https://developer.apple.com/account) and navigate to [Certificates, IDs & Profiles > Identifiers](https://developer.apple.com/account/resources/identifiers/list).
* Select your app's Bundle ID.
* Scroll down the **Capabilities** list and enable **Sign In with Apple**.
* Click **Save** and confirm.

## Natively Dashboard Setup

Before proceeding, make sure you have completed the [App Capabilities Configuration](#app-capabilities-configuration) step above.

* Open your Natively app dashboard and navigate to **Features** > **Social Auth** > **Sign In with Apple (iOS only)**.
* Toggle the feature to **Enabled**.
* Click **Save**.
* Rebuild your iOS app.

{% hint style="warning" %}
You must rebuild your app for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### &#x20;Setup logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### Natively - Apple Sign In

#### Events:

* Apple Sign In Success
* Apple Sign In Failed

#### States:

* Email
* Given Name
* Family Name
* Error
* Subject - unique identifier based on your app + user iCloud (it's unique)
* Initial Sign In - means that the user signs in/signs up for the first time to your app

#### Actions:

* Sign In with Apple

#### How to use Sign In with Apple in Bubble

* Add **Natively - Apple Sign In** element on the page.

* Create a "Sign In with Apple" button (you can use a simple image or HTML/CSS). Find some examples [here](https://appleid.apple.com/signinwithapple/button).<br>

  <figure><img src="/files/4XeJbhmj4KQ1vKZayFh8" alt=""><figcaption></figcaption></figure>

* Add "Apple Sign In Success" event and the following actions<br>

  <figure><img src="/files/yeCZY41yKYcMnXrNWXYv" alt=""><figcaption></figcaption></figure>

{% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY SIGN IN WITH APPLE - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const appleService = new NativelyAppleSignInService();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// appleService.signin(callback);
//   - Triggers the native Apple Sign In prompt.

// ============================================================================
// CALLBACK RESPONSE FIELDS
// ============================================================================
// resp.status           - Boolean. true if sign-in was successful, false if failed.
// resp.email            - The user's email. Only available on the first sign-in.
// resp.subject          - Unique identifier tied to the user's iCloud account.
//                         Use this as your primary user identifier in your database.
// resp.givenname        - The user's first name. Only available on the first sign-in.
// resp.familyname       - The user's last name. Only available on the first sign-in.
// resp.initial          - Boolean. true if this is the user's first sign-in to your app.
// resp.jwt              - A JWT issued by Apple for the authenticated user.
// resp.authorizationCode - A short-lived verification code for this authorization.
//                         Must be validated by your server with Apple within 5 minutes.
// resp.message          - Error message. Only present if resp.status is false.

// --- Sign In with Apple. Example. Start ---

const apple_signin_callback = function(resp) {
    if (resp.status) {
        console.log(resp.email);
        console.log(resp.subject);          // unique identifier based on your app + user iCloud (it's unique)
        console.log(resp.givenname);
        console.log(resp.familyname);
        console.log(resp.initial);          // true if user signed in/up for the first time
        console.log(resp.jwt);              // JWT issued by Apple for the authenticated user
        console.log(resp.authorizationCode); // must be validated server-side within 5 minutes
    } else {
        console.log(resp.message);
    }
};

appleService.signin(apple_signin_callback);

// --- Sign In with Apple. Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.

Copy the line below and paste it into your AI agent.

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Sign In with Apple SDK: const appleService = new NativelyAppleSignInService(); // appleService.signin(callback) — triggers the native Apple Sign In prompt. // Callback response: resp.status (boolean) — true if successful. resp.email — user email, only available on first sign in. resp.subject — unique user identifier, use as primary user ID. resp.givenname — first name, only on first sign in. resp.familyname — last name, only on first sign in. resp.initial — true if first sign in. resp.jwt — JWT issued by Apple. resp.authorizationCode — must be validated server-side within 5 minutes. resp.message — error message if status is false. For reference: https://docs.buildnatively.com/natively-platform/features/social_auth/sign-in-with-apple
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Add a Sign in with Apple button on the login page that creates a new user account on first sign in and logs in returning users".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### User identification

Always use `subject` as your primary user identifier - it's unique per user per app and never changes, even if the user switches devices or hides their email.

#### Email availability

Apple lets users hide their real email behind a private relay address. This relay address is only returned on the **first sign-in**; on all subsequent logins `email` will be empty if the user chose to hide it. Design your sign-up flow to handle this: capture the email on first login and store it; never rely on it being present on repeat logins.

#### Name availability

Same as email - `givenname` and `familyname` are only returned on the **first sign-in**. Store them immediately after the first successful login. On subsequent logins, these fields will be empty.

#### Show the Sign In with Apple button only on iOS

Since **Sign In with Apple** only works on iOS, make sure the button is only visible to iOS users. Use the [Browser Info](/guides/integration/browser-info) feature to detect whether the app is running on iOS, Android, or a web browser, and conditionally show or hide the button accordingly.

#### First-time vs returning users

Use `resp.initial` to distinguish between a new sign-up and a returning login. Run your account creation logic only when `initial` is `true`.

#### Resetting Sign In with Apple for testing

To simulate a first-time sign-in during development, go to **iOS Settings** > **\[Your Name]** > **Password & Security** > **Apps Using Your Apple ID**, find your app, and tap **Stop Using Apple ID**. Your next sign-in will return all fields as if it's a fresh account.

## Troubleshooting

<details>

<summary>The Sign-In button is not working or doing nothing</summary>

Likely causes: feature not enabled in Natively Dashboard, app not rebuilt after enabling, or the Natively Sign In with Apple JS SDK is not being used for this type of login.&#x20;

</details>

<details>

<summary>Email is empty on login</summary>

Expected behavior if the user chooses to hide their email - not a bug. Should be handled in the sign-up flow.

</details>

<details>

<summary>Name fields are empty on login</summary>

Expected behavior on all logins after the first. Should be stored on first sign-in.

</details>

<details>

<summary>Sign-in succeeds, but the user is treated as new every time</summary>

Likely caused by using `email` as the primary identifier instead of `subject`.

</details>

<details>

<summary>User cancelled error</summary>

When the user dismisses the Apple Sign In prompt, `resp.status` returns `false`. Should be handled gracefully without showing an error to the user.

</details>

<details>

<summary>Testing reset not working</summary>

If "Stop Using Apple ID" doesn't appear for the app, the app may not have completed a successful sign-in yet.

</details>

[^1]: Replace this placeholder


# Publish

## Video Guide

{% embed url="<https://youtu.be/ivHtI1oVZ3E>" %}

{% content-ref url="/pages/obtTnxOB16KQPFvJrN5T" %}
[iOS App](/natively-platform/app-info/ios-build)
{% endcontent-ref %}

{% content-ref url="/pages/vkG0ra8unWXwredvn14v" %}
[Android App](/natively-platform/app-info/android-build)
{% endcontent-ref %}


# iOS App

Configure your iOS app in Natively Dashboard to generate builds and connect to App Store Connect.

{% embed url="<https://app.arcade.software/share/3xBLgiVJIbZpn8Axtn8e>" %}

## Prerequisites

* Create an [Apple account](https://appleid.apple.com/account?appId=632\&returnUrl=https%3A%2F%2Fdeveloper.apple.com%2Faccount%2F)
* [Purchase an Apple Developer membership](https://developer.apple.com/programs/enroll/). Please see [this](https://developer.apple.com/programs/) link for more details on the Apple Developer program.

## App Store Credentials&#x20;

#### Generate API Key:

* Open an [App Store Connect](https://appstoreconnect.apple.com/) page and navigate to [Users and Access > Integrations](https://appstoreconnect.apple.com/access/integrations/api)

{% hint style="info" %}
In the [organization](https://developer.apple.com/programs/enroll/) type of Apple account, only **Admins** or the **Owner** can access this tab. But in [personal](https://developer.apple.com/programs/enroll/) only the **Owner**.
{% endhint %}

* If you see the **Request Access** button, click on it.
* If you didn't create any keys before, click **Generate API Key**. Otherwise, click the **Add button (+)**.
* A pop-up will appear. Enter your API Key Information:
  * **Name:** Enter a name for the key. This is a reference and is not part of the key itself.
  * **Access:** Select the access type **App Manager**.
* Click **Generate**.
* Find the row for the API Key you just generated and click **Download.** A pop-up will appear, select **Download**.

![](/files/UnmkzH7sPmBiIjGnjSml)

{% hint style="warning" %}
The .p8 file can only be downloaded once. If it's lost, you'll need to revoke the key and generate a new one.
{% endhint %}

#### **Add the API Key to Natively**

After generating the key, you'll need three values for the Natively Dashboard:

* **Issuer ID** - found at the top of the Integrations page, above the table of keys.
* **Key ID** - shown in the row of the key you just created.
* **API Key (.p8 file)** - the file you downloaded in the previous step.

In the Natively Dashboard, navigate to **Publish > iOS Build** and enter the Issuer ID, Key ID, and upload the .p8 file in the corresponding fields.

<figure><img src="/files/JXZeUsmifzIJQRnbh5h2" alt=""><figcaption></figcaption></figure>

## Bundle Identifier

{% hint style="warning" %}
The Bundle Identifier is a unique identifier for your app. Once set, it can only be changed if your app or developer account is suspended.
{% endhint %}

You have two options for setting your Bundle Identifier: let Natively [generate](#generate-bundle-id) one automatically, or [create your own](#create-your-own-bundle-id) in your Apple Developer account.

<figure><img src="/files/owlxRscqLSFfquPwwluP" alt=""><figcaption></figcaption></figure>

### Generate Bundle ID

Natively can automatically generate a Bundle Identifier for your app. This is the simplest option if you don't already have one.

### Create Your Own Bundle ID

If you'd prefer to use your own Bundle Identifier, you can create one in your Apple Developer account.&#x20;

A Bundle ID is a unique string (in reverse domain format, e.g. <mark style="color:red;">`com.yourcompany.appname`</mark>) that identifies your app within the Apple ecosystem.

{% hint style="info" %}
If it's a personal Apple account, only the owner can create a Bundle ID.
{% endhint %}

Follow these steps to create a Bundle ID, or use this [shortcut link](https://developer.apple.com/account/resources/identifiers/bundleId/add/bundle) to skip directly to the creation form:

* Open the [Apple Developer homepage](https://developer.apple.com/account), under **Program resources** > **Certificates, IDs & Profiles**, select [**Identifiers**](https://developer.apple.com/account/resources/identifiers/list).
* Click on the [**Add button (+)**](https://developer.apple.com/account/resources/identifiers/add/bundleId).
* In Register a new identifier, select **App IDs**, and click **Continue**.
* In Select a type, choose **App**, and click **Continue**.
* Enter the [App Bundle Information](https://help.apple.com/app-store-connect/#/deveaec374de):
  * **Bundle ID:** Enter a package name (i.e., <mark style="color:red;">`com.yourcompany.appname`</mark>)
  * **Description:** Provide a short description of your app (can be anything, this will not appear in the App Store).
  * **Capabilities:** skip for now.
* When everything is done, click **Continue**, then **Register.**

![](/files/n8BY1ZPrczmALWgPaHGP)

* Return to our platform and fill the **Bundle Identifier** field with Bundle ID

{% hint style="info" %}
Don't copy the **App ID Prefix** shown alongside your Bundle ID - that's a separate value and is not the same as the App Store App ID used later in this guide.
{% endhint %}

### Demo

![Create Your Own Bundle Id](/files/pR6wEBPgSzbaP4UzBq2f)

## App Store App ID

[App Store Connect](https://help.apple.com/app-store-connect/#/dev2cd126805) is used to submit apps to the App Store, manage apps, and more.

* Navigate to [App Store Connect](https://appstoreconnect.apple.com/) and then select [My Apps](https://appstoreconnect.apple.com/apps).
* Click on the **Add button (+)** and then select **New App.**
* A pop-up will appear. Enter your app information:
  * **Platform:** for mobile apps, it's **iOS**.
  * **Name:** Enter a Name for your app (this is the name that will show in the App Store).
  * **Primary Language** for your app.
  * **Bundle ID:** Select the **Bundle ID** you created in the [previous step](#bundle-identifier).
  * **SKU**: Enter a unique identifier. You can also add your **Bundle ID** here, as long as it is unique.
  * User Access: **Set the user access.** If you select Limited Access, you will need to select the users you would like to be able to access this app. This will only appear if you have other users included in your App Store Connect account.
* When you are done, select **Create.**

![](/files/EiWZ2M6rqpWWYFXPvEfm)

{% hint style="info" %}
It can take a few minutes for your app to be created. If it doesn't appear right away, reload the page.
{% endhint %}

* Select **App Information** (under **General** in the left sidebar).
* Scroll down to **General Information** and find the field labeled **Apple ID**. This is a numeric identifier for your app - not to be confused with your personal Apple ID account.
* Select the **Apple ID** and copy it.

![](/files/Nadpi2T5jXliboTw3ZHp)

* Return to the Natively Dashboard and enter this value in the **App Store App ID** field.

### Demo

![Create App in App Store Connect](/files/SzeSETNdRcUiR0PprEa8)


# Android App

Configure your Android app in Natively Dashboard to generate AAB and APK build files.

## Bundle Identifier

{% hint style="warning" %}
The Bundle Identifier is a unique identifier for your app. Once set, it can only be changed if your app or developer account is suspended.
{% endhint %}

### Generate Bundle ID

The simplest option - Natively automatically generates a valid, unique Bundle Identifier for your app.&#x20;

### Create Own Bundle ID

On Android, the Bundle Identifier is called a **Package Name**. It's the unique ID that identifies your app across the entire Google Play Store, and it follows a Reverse-DNS format based on a domain you control - for example `com.yourcompany.appname`. Once you publish your app with a given Package Name, you can never change it, so it's worth getting right from the start.

Google Play enforces a strict set of rules for Package Names. If your Package Name doesn't follow these, your build will fail validation:

* Must follow the Reverse-DNS convention (e.g., `com.yourcompany.appname`).
* Must contain at least two segments separated by a period.
* Can only contain letters, numbers, underscores (\_), and periods (.) - no hyphens and no spaces.
* Each segment must start with a letter, not a number.
* No Hyphens: Android Package Names cannot contain hyphens (`-`).
* Avoid using reserved Java keywords like `abstract, assert, boolean, break, byte, case, catch, char, class, const, continue, default, do, double, else, enum, extends, final, finally, float, for, goto, if, implements, import, instanceof, int, interface, long, native, new, package, private, protected, public, return, short, static, strictfp, super, switch, synchronized, this, throw, throws, transient, try, void, volatile, while`.
* Must be globally unique across the Google Play Store - if someone else has already published `com.myapp.test`, you can't use it.

#### **Checking if a Package Name is available**

Before settling on a Package Name, check whether it's already taken by visiting:

`https://play.google.com/store/apps/details?id=YOUR_PACKAGE_NAME`

If the page shows "not found," the name is likely available. If an app page loads - even for an old or unrelated app - that name is taken, and you'll need to choose another.

Once you've decided on a Package Name, enter it in the **Bundle Identifier** field under **Publish > Android Build** in the Natively Dashboard.

<figure><img src="/files/pYWBvyKB5XOu3anYoPxj" alt=""><figcaption></figcaption></figure>

## Google Play Console requirements

Before you create your app listing in Google Play Console, it's worth understanding how Google Play handles package names - this will help you avoid issues later when you're ready to publish.

#### Package names are permanent

Once you upload your first build to Google Play Console under a given package name, that name is locked to your app forever. Package names can't be deleted or reused in the future, so choose carefully. If you need to change it later, you'd have to publish as a new app with a new listing - you can't reuse or migrate the package name.

#### Package names must be globally unique

Your app's package name must be unique across all of Google Play, not just within your own developer account - if another app already uses the package name you chose, you'll need to recompile with a different one.

#### First upload matters

Google Play Console creates the app listing for your package name based on your first uploaded build. Make sure the package name you set in Natively matches the one you intend to use for your Play Console listing before generating your build.


# Settings

Do not forget to save your changes and rebuild your app.

## General App Info

### App Name

The name of your application. Will be displayed on a device.

### App Url

The URL of your website that runs inside of your application.

{% hint style="info" %}
If your app is password protected (and it's not displayed in the app), please use the following formatting for your URL\
https\://{username}:{password}@yourwebsiteurl.com (example: <https://roman:123456@yourwebsiteurl.com>)
{% endhint %}

### External App Schemes

These app schemes will allow to open external apps from yours. \
Write the scheme in the input and click the 'Add' button.

<figure><img src="/files/FfcMbKI7xBp9FWwLX6GB" alt=""><figcaption></figcaption></figure>

### Internal URLs

Keep trusted third-party domains (like your own Help Center) inside your app’s primary view. This removes the "browser" UI (navigation bars) and makes external content feel like a native part of your app.

By default, Natively will open external domains in an In-App Browser to protect the user's session. However, you can force specific external URLs to open within your app’s primary view (without the navigation bar or "Close" ison) by using the whitelist.

How to whitelist a domain:

1. Navigate to your Natively Dashboard.
2. Go to Settings.
3. Locate the Internal URLs section.
4. Add the domains you want to keep internal (e.g., `*.example.com`).

When to use this:

* If your app logic spans multiple subdomains (e.g., `app.example.com` and `auth.example.com`).
* If you use a third-party service that must stay within your app context.

Examples:

* \*.example.com matches app.example.com
* example.\*.com matches example.dev.com
* *\*.\**.example.com matches api.dev.example.com

\
Write the URL/Domain in the input and click the 'Add' button.

<figure><img src="/files/b9Y014H6IKlwtji5pS3f" alt=""><figcaption></figcaption></figure>

## iPad support \[Only for iOS]

* Select devices that you want to be supported
* iPhone, iPad or both
* **Android** tablets are supported by default

{% hint style="info" %}
If your previously submitted version included iPad support, it's crucial to keep this setting consistent in subsequent builds.
{% endhint %}

## Reload WebView \[Advanced]

Reload the WebView after a period in the background or screen lock, or manually trigger a reload.

1. Enable the feature, set time interval (seconds), save, and rebuild.
2. Manual reload:

&#x20;      2.1. Bubble: "Natively - Reload WebView" action.&#x20;

&#x20;      2.2. JS SDK: window\.natively.reloadWebview();


# Integration (Native Features)

Natively allows you to set up many features. Pick up most suitable for your business.

## Available Native Features

1. Push Notifications with OneSignal.
2. Fetching device info
3. Geolocation (Foreground/Background live location tracking)
4. Store data in local storage
5. Biometrics authorization and storing user's credentials
6. Send SMS/Email through native screens
7. Native Date Picker
8. Native Camera (Take a photo or record a video)
9. QR/Barcode scanner
10. Native contacts (fetch all contacts or create new)
11. In-App Purchases (RevenueCat)
12. Native toast and banner
13. Share sheet for photos, files, text and URLs
14. Open external URL inside of the app (In-App browser)
15. Open external installed apps
16. Request user's review
17. vCARD(.vcf) files handling
18. HTML5 fullscreen video
19. tel:,sms:,mailto: handling
20. File download (through Native share sheet)
21. AppsFlyer Analytics
22. Facebook Analytics
23. Apple's HealthKit Integration
24. Social (Google, Facebook, Telegram) auth support (through the web)
25. Custom launch screen
26. Pull to refresh
27. Swipe navigation
28. Custom app icons
29. Status bar customization
30. Audio recording
31. Debugger console
32. Admob integration
33. NFC Read/Write
34. Apple Sign In
35. [And more coming soon](https://ideas.buildnatively.com)


# How to get started?

Everything you need to integrate the Natively JavaScript SDK or Bubble plugin into your project.

Natively provides several ways to integrate native features into your web app: the **JavaScript SDK** for loading via CDN script tag, **Bubble plugin** for no-code Bubble apps, **NPM package** for framework-based projects like React or Next.js, and support for **AI-powered editors** like Lovable, Base44, and Replit. This page covers the global setup and configuration that applies across all features. For feature-specific implementation, refer to the individual feature pages.

{% tabs %}
{% tab title="Bubble.io Plugin" %}
The fastest way to add native features to your Bubble app without writing any code.

#### **Installation**

* Navigate to the **Plugins** tab in your Bubble editor.
* Search for [Natively](https://bubble.io/plugin/natively---fast-mobile-app-wrapper-1654595882459x381599056563798000) and click **Install**.

<figure><img src="/files/kkMIxu1yrBM6w6j2ianv" alt=""><figcaption></figcaption></figure>

#### **Configuration (Headers)**

The **mode** field in the plugin settings controls environment-specific behavior:

* `debug` - enables detailed error alerts and console logs. Recommended during development.
* `preview` - Required for testing Push Notifications within the [Natively Preview app](/natively-platform/preview). Remove this tag before testing your own build.

#### **Configuration (API Keys)**

* `onesignal_appId` - links your mobile app to your specific OneSignal project to enable device registration.
* `onesignal_apiKey` - authorizes your Bubble web app to trigger [push notifications](/natively-platform/features/notifications/onesignal-push-notifications) via OneSignal.
* `revenuecat_apiKey` - Allows your Bubble web app to verify purchases and sync subscription statuses with RevenueCat ([In-app purchases](/hidden-pages/in-app-purchases)).

<figure><img src="/files/IZMxm0lqKcavif7ag8mD" alt=""><figcaption></figcaption></figure>

Explore our Example App to see a full configuration of the Natively plugin. Use this sandbox to inspect functional workflows, element states, and API setups.

* Editor Link: [Natively QA Sandbox](https://bubble.io/page?name=index\&id=nativelyqa\&tab=tabs-1)
* Credentials: `Username: 1` / `Password: 1`

{% hint style="warning" %}
Avoid calling **Get** actions (like getting Device Info) on Page Load. The Natively plugin needs a few milliseconds to inject into your site. Use a slight delay or trigger actions based on user interaction to ensure the plugin is active.
{% endhint %}

{% hint style="warning" %}
Plugin elements must be set to **Visible on page load** to initialize correctly. Place them directly on the page root - not inside hidden containers such as Popups, Floating Groups, Group Focus elements, or Repeating Groups. To hide an element from your UI, set its dimensions to **0x0 px**.
{% endhint %}
{% endtab %}

{% tab title="JavaScript SDK" %}
Include the Natively SDK to bridge the gap between your web code and the mobile OS. Add the following to your `<head>` tag:

```javascript
<head>
  <script
    async
    onload="nativelyOnLoad()"
    src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js">
  </script>

  <script>
    function nativelyOnLoad() {
      // The SDK is now successfully injected into the global window object.

      // --- DEBUG MODE ---
      // Set to TRUE during development: Shows native OS alerts if an SDK error occurs.
      // Set to FALSE or not included in production: Prevents users from seeing raw error messages.
      window.natively.setDebug(true);

      console.log("✅ Natively SDK loaded successfully.");

      // Add any other automatic initializations here:
      // e.g., Check permissions, initialize analytics, or setup your audio player.
    }
  </script>
</head>
```

{% hint style="info" %}
You can target a specific SDK version by modifying the version number in the CDN URL (e.g., `2.26.0`).

* Standard Syntax: `https://cdn.jsdelivr.net/npm/natively@VERSION_NUMBER/natively-frontend.min.js`
* To ensure you are using the most up-to-date features, refer to the [Natively GitHub Repository](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
  {% endhint %}
  {% endtab %}

{% tab title="npm Package" %}
**Installation**

Install the Natively package via npm.

```bash
npm install natively@">=2.26.0"
```

**Usage**&#x20;

```typescript
import { NativelyInfo, useNatively } from 'natively';

// useNatively() is a safe React wrapper around window.natively
const natively = useNatively();

// All Natively SDK classes work the same as with the CDN approach
const info = new NativelyInfo();
const browserInfo = info.browserInfo();
```

{% hint style="danger" %}
The Natively SDK relies on the browser's `window` object and cannot run server-side. If you are using a framework with server-side rendering (Next.js, Nuxt, SvelteKit, Remix, etc.), ensure that Natively code only runs on the client side. In Next.js App Router, declare `'use client';` at the top of any file that uses the Natively SDK.
{% endhint %}

```typescript
'use client'; // <--- CRITICAL: Natively cannot be executed on the server-side 

import { NativelyInfo, useNatively } from 'natively';

export default function NativeDashboard() {
  const natively = useNatively();
  const info = new NativelyInfo();
  const browserInfo = info.browserInfo();

  const handleOpenConsole = () => {
    if (browserInfo.isNativeApp) {
      natively.openConsole();
    } else {
      console.warn("Console command ignored: You are viewing this in a standard web browser.");
    }
  };

  return (
    <div>
      <p>Running inside Native App: <strong>{browserInfo.isNativeApp ? "✅ True" : "❌ False"}</strong></p>
      <button onClick={handleOpenConsole}>Open Natively Debug Console</button>
    </div>
  );
}
```

Example project: [Next.js boilerplate](https://github.com/romanfurman6/nextjs-boilerplate)

{% hint style="info" %}
You can target a specific version by modifying the version number (e.g., `2.26.0`).

* Standard Syntax: `npm install natively@">=VERSION_NUMBER"`
* To ensure you are using the most up-to-date features, refer to the [Natively GitHub Repository](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
  {% endhint %}
  {% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}


# Apple ATT

This documentation is designed to help you implement the App Tracking Transparency (ATT) framework in your Natively app. This is required if your app collects data used for tracking or accesses the device's advertising identifier (IDFA).

Apple’s definition of "tracking" specifically refers to linking data collected from your app with third-party data for advertising or sharing that data with a data broker.

<figure><img src="/files/k3Kx1SX63CVZ05Dn0DrW" alt="" width="180"><figcaption></figcaption></figure>

### "Is it Tracking?" Checklist

Here is the breakdown of when you actually need to ask for tracking permissions:

* Ads (AdMob, Meta, etc.): YES. If users deny, you must serve non-personalized ads (or no ads) and stop sending the IDFA.
* Third-Party Analytics (Meta Pixel, Google Analytics): YES. If these tools track the user's behavior across other websites or apps to build a profile, you must ask.
* First-Party Analytics: NO. If you are just tracking what the user does inside your app to improve the UI (e.g., "User clicked Button A"), and you aren't sharing that with others for ad-targeting, ATT is technically not required.
* Authentication/Fraud: NO. You don't need ATT for strictly functional things like keeping a user logged in or preventing bot attacks.

{% hint style="info" %}
If AdMob or Analytics features are enabled in your Natively dashboard, the App Tracking Transparency (ATT) dialog will be triggered automatically on the first app launch to ensure compliance with Apple’s privacy guidelines.
{% endhint %}

{% hint style="info" %}
Web-Based Analytics: Even if you use a web-based tracking solution, you still need to implement Apple ATT if that web solution collects identifiers for tracking.
{% endhint %}

## Natively Dashboard Setup

* Navigate to Features > Apple ATT in your app's dashboard.
* Enable Apple ATT and provide the Permission Description text. The Permission Description is a mandatory string that Apple displays to the user explaining why you are requesting tracking permission.
* Click **Save**.

{% hint style="info" %}
**Permission Description:** Be honest and clear. Apple frequently rejects apps that provide vague descriptions like "We need this to improve the app." Instead, use: "We use your data to provide personalized advertisements that are relevant to your interests."
{% endhint %}

<figure><img src="/files/95Fzpp3TY3oiOzhjInff" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
You must rebuild your application for these changes to take effect.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code) or **JavaScript SDK** (Code).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button , search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.25.2`) .

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.25.2/natively-frontend.min.js"></script>
</head>
```

{% endtab %}
{% endtabs %}

### Setup logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Drag the Natively - ATT element onto your page.**

{% hint style="warning" %}
This element must be set to Visible on page load to initialize correctly. It should be placed directly on the page root and not inside hidden containers, such as Popups, Floating Groups, Group Focus elements, or Repeating Groups. To hide the element from your UI, you may set its dimensions to 0x0 px.
{% endhint %}

**Element Logic (Events, States, & Actions)**

Use the following data points to build your logic.

* Events:
  * ATT is authorized: Fires whenever the ATT is successfully authorized.
  * ATT is not authorized: Fires whenever the ATT is denied.
* States:
  * `Status` (Text): Returns the permission status: `authorized | denied | notDetermined | restricted | notSupported` .
  * `isAuthorized` (Yes/No): Returns `yes` if permission is authorized.
* Actions:
  * Show ATT: Triggers the native system dialog using the "Permission description" defined in your dashboard.
  * Get ATT's Status: Manually refreshes the `Status` and `isAuthorized` states. Useful if you need to ensure the current status before a specific workflow.
    {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY APP TRACKING TRANSPARENCY (ATT) - DOCUMENTATION & EXAMPLES
// ============================================================================
// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// natively.attGetStatus(callback); 
//    - Retrieves the current tracking authorization status.
//    - Does NOT trigger a popup. Use this to check status.
//
// natively.attShowPopup(callback); 
//    - Triggers the native iOS ATT permission dialog.
//    - Note: This popup will only appear ONCE per app installation. 
//    - If the user has already decided, the callback returns the existing status 
//      immediately.


// --- ATT Implementation. Start ---

/**
 * 1. DEFINE THE CALLBACK
 * This function handles the response object returned by the Natively SDK.
 */
const handleAttResponse = (resp) => {
    // resp.status (string): 'authorized', 'denied', 'notDetermined', 'restricted', 
    // or 'notSupported'
    // resp.isAuthorized (boolean): true if authorized, false otherwise
    
    console.log("ATT Status:", resp.status);
    console.log("Is Authorized:", resp.isAuthorized);
    
    if (resp.isAuthorized) {
        // Handle logic for authorized users (e.g., initialize tracking SDKs)
        console.log("User granted tracking permission.");
    } else {
        // Handle logic for non-authorized users
        console.log("User declined or restricted tracking.");
    }
};

/**
 * 2. CHECK STATUS
 * Call this when the app or page loads to determine current permissions.
 */
function checkStatus() {
    if (window.natively) {
        natively.attGetStatus(handleAttResponse);
    } 
}

/**
 * 3. REQUEST PERMISSION
 * Call this when the user interacts with a UI element (like an 'Accept' button).
 */
function requestPermission() {
    if (window.natively) {
        natively.attShowPopup(handleAttResponse);
    }
}

// --- ATT Implementation. End ---


// ============================================================================
// UNDERSTANDING THE RESPONSE
// ============================================================================
/**
 * The 'status' string returned in the callback can be:
 * * 'authorized'    -> User granted permission. 
 * (isAuthorized: true)
 * * 'denied'        -> User explicitly denied permission. 
 * (isAuthorized: false)
 * * 'notDetermined' -> User hasn't seen the prompt yet. 
 * (isAuthorized: false)
 * * 'restricted'    -> Tracking is restricted at the OS level (e.g., parental controls). 
 * (isAuthorized: false)
 * * 'notSupported'   -> The device or OS version does not support ATT (e.g., Android or old iOS).
 * (isAuthorized: false)
 */
```

{% endtab %}
{% endtabs %}

### Auto-Prompt vs. Manual

The behavior of the ATT pop-up changes depending on which other features (like Analytics or AdMob) are enabled in your app.

| Feature Combination        | Auto-Prompt on Launch? | Manual Trigger (attShowPopup) |
| -------------------------- | ---------------------- | ----------------------------- |
| ATT Only                   | ❌ No                   | ✅ Supported                   |
| ATT + Analytics (or AdMob) | ✅ Yes                  | ❌ Not Supported               |
| ATT + Analytics + AdMob    | ✅ Yes                  | ❌ Not Supported               |
| Analytics + AdMob (No ATT) | ✅ Yes                  | ❌ Not Supported               |


# App Storage

Store and retrieve persistent key-value data on the user's device across app sessions.

## What is App Storage?

App Storage lets your app save data directly on the user's device using a native key-value store. Unlike browser localStorage, stored values persist across app sessions and are not cleared when the browser cache is cleared. This makes it useful for storing user preferences, session tokens, onboarding state, or any other data that needs to survive app restarts.

{% hint style="warning" %}
Storing values larger than 1.42MB in a single write may cause the app to crash. Keep individual values small and split large data across multiple keys if needed.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Element] Natively - Storage

#### Events:

* **Storage value received** - fires when a value is successfully retrieved by key.

#### States:

* **Latest requested storage value** - the value returned for the most recently requested key.
* **Latest requested storage key** - the key used in the most recent request.

#### Actions:

* **Store value to device storage** - saves a value under the specified key:
  * key - the identifier for the stored value;
  * value - the data to store.
* **Get value from device storage** - retrieves a value by key. Listen to the **Storage value received** event to access the result:
  * key - the identifier to look up.
* **Remove value from device storage** - deletes a specific key and its value:
  * key - the identifier to remove.
* **Reset device storage** - clears all data stored by your app.
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY APP STORAGE - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const storage = new NativelyStorage();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// storage.setStorageValue(key, value)
//   - Saves a value under the specified key.
//   - key: string - the identifier for the stored value
//   - value: string - the data to store
//   - No callback required.
//
// storage.getStorageValue(key, callback)
//   - Retrieves a value by key.
//   - key: string - the identifier to look up
//   - Returns the value via callback.
//
// storage.removeStorageValue(key)
//   - Deletes a specific key and its value.
//   - key: string - the identifier to remove
//   - No callback required.
//
// storage.resetStorage()
//   - Clears all data stored by your app.
//   - No callback required.

// ============================================================================
// CALLBACK RESPONSE FIELDS
// ============================================================================
// resp.key   - The key that was requested.
// resp.value - The stored value. Returns null if the key does not exist.

// --- App Storage. Store and Retrieve Example. Start ---

const key = "user_preference";
const value = "dark_mode";

// Store a value
storage.setStorageValue(key, value);

// Retrieve a value
const get_storage_callback = function(resp) {
    if (resp.value === null) {
        console.log("No value found for key:", resp.key);
        return;
    }
    console.log("Retrieved value:", resp.value);
};

storage.getStorageValue(key, get_storage_callback);

// --- App Storage. Store and Retrieve Example. End ---


// --- App Storage. Remove and Reset Example. Start ---

// Remove a specific key
storage.removeStorageValue(key);

// Clear all stored data
storage.resetStorage();

// --- App Storage. Remove and Reset Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively App Storage SDK: const storage = new NativelyStorage(); // storage.setStorageValue(key, value) — saves a value under the specified key. No callback required. // storage.getStorageValue(key, callback) — retrieves a value by key. resp.key: the requested key. resp.value: the stored value, null if key doesn't exist. // storage.removeStorageValue(key) — deletes a specific key and its value. No callback required. // storage.resetStorage() — clears all data stored by your app. No callback required. For reference: https://docs.buildnatively.com/guides/integration/app-storage
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Save the user's selected language preference to device storage and load it automatically on app start".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Persisting user preferences

Use App Storage to save user settings like theme (light/dark), language, notification preferences, or any other configuration that should persist between sessions. Store the value on change and retrieve it on app load to restore the user's last state.

#### Session and authentication tokens

App Storage is a good place to store session tokens or lightweight authentication data that needs to survive app restarts. Unlike browser localStorage, this data won't be wiped when the browser cache is cleared.

#### Onboarding and first-run state

Store a flag like `onboarding_complete: true` after a user finishes onboarding. On app load, check this value to decide whether to show the intro screens or go straight to the main app.

#### Always handle the null case

If you request a key that doesn't exist, `resp.value` returns `null`. Always check for this before using the value - especially on first launch, before any data has been stored.

#### Keep values small

App Storage is designed for lightweight key-value data. Storing values larger than 1.42MB in a single write may cause the app to crash. If you need to store larger data, split it across multiple keys or consider a different storage approach.

#### Keys are strings only

Both keys and values must be strings. If you need to store objects or arrays, serialize them with `JSON.stringify()` before storing and parse with `JSON.parse()` after retrieving.

## Troubleshooting

<details>

<summary><code>resp.value</code> returns <code>null</code></summary>

The key does not exist in storage yet. This is expected on first launch or if the key was never set. Always check for `null` before using the retrieved value.

</details>

<details>

<summary>Stored value not persisting between sessions</summary>

Make sure you are using `storage.setStorageValue()` and not browser localStorage - localStorage can be cleared by the system or the user. If the value is being set correctly but not retrieved after a restart, verify the key name is exactly the same in both the set and get calls.

</details>

<details>

<summary>App crashes when storing a value</summary>

The stored value likely exceeds the 1.42MB limit for a single write. Split the data across multiple smaller keys or reduce the size of the value being stored.

</details>

<details>

<summary><code>resetStorage()</code> cleared data I didn't expect</summary>

`resetStorage()` clears all data stored by your app - not just a specific key. Use `removeStorageValue(key)` if you only want to delete a specific entry.

</details>

<details>

<summary>Storing objects or arrays doesn't work</summary>

App Storage only supports string values. Serialize objects with `JSON.stringify()` before storing and deserialize with `JSON.parse()` after retrieving.

</details>

[^1]: Replace this placeholder


# Audio Player

Native Audio Player allows you to integrate background audio playback into your mobile app. By bridging your web app with native iOS and Android audio engines, you can provide a seamless listening experience that continues even when the app is minimized or the device is locked.

## Natively Dashboard Setup

Before your app can communicate with the device's hardware, the Audio Player capability must be activated in your build settings. This step injects the necessary native code into your iOS and Android builds.

1. Go to Features > Notifications > Audio Player.
2. Switch it to Enabled.
3. Save Changes.

<figure><img src="/files/Oj1x62J1Q5LN8zHo3fdp" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
You must rebuild your application for these changes to take effect.
{% endhint %}

### Implementation <a href="#implementation" id="implementation"></a>

Choose your integration method below: **Bubble.io Plugin** (No-Code) or **JavaScript SDK** (Code).

#### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}

**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

If it is NOT installed: Click the + Add plugins button , search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="Javascript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.25.2`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.25.2/natively-frontend.min.js"></script>
</head>
```

{% endtab %}
{% endtabs %}

#### Setup logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}
Drag the Natively - Audio Player element onto your page.

{% hint style="warning" %}
This element must be set to Visible on page load to initialize correctly. It should be placed directly on the page root and not inside hidden containers, such as Popups, Floating Groups, Group Focus elements, or Repeating Groups. To hide the element from your UI, you may set its dimensions to 0x0 px.
{% endhint %}

<figure><img src="/files/iCU0wLK16yEzLExXzDUq" alt=""><figcaption></figcaption></figure>

{% hint style="warning" %}
For the Queue Track list to display data, set the AudioPlayerTrack (Natively  - Objects) data type.
{% endhint %}

<figure><img src="/files/au3CtOMdGa0zllS3V3f4" alt=""><figcaption></figcaption></figure>

**Element Logic (Events, States, & Actions)**

These triggers allow you to run workflows when the player's status changes on the device.

* Events:
  * Track Started: Fires as soon as audio begins coming out of the device's speakers. Use this to change your UI to a "Now Playing" mode.
  * Playback Error: Triggered if a file fails to load, a URL is broken, or the network connection is lost. The specific reason will be available in the **Error Message** state.
  * Item Added To Queue: Fires after a successful `Queue Add` action.
  * Metadata Updated: Triggered whenever the track’s title, artist, or artwork is successfully updated with the `Set Metadata` action.
  * Set Metadata Error: Fires if the player fails to update the song information (usually due to a malformed JSON in the "Extras" field).
  * Queue Refreshed: Triggered after the `Queue Get` action successfully retrieves the full list of tracks and populates the **Queue Tracks** state.
* States:
  * `Status` (Text): Returns the current operational status (e.g., `SUCCESS`).
  * `Queue Length` (Number): The total count of tracks in the current list.
  * `Current Index` (Number): The position of the track currently playing.
  * `Is Playing` (Yes/No): Returns Yes if audio is active.
  * `Error Message` (Text): Returns the description of the error encountered during a failure.
  * `Queue JSON` (Text): The raw technical data of the entire queue.
  * `Queue Track` (List of AudioPlayerTrack (Natively  - Objects)): The actual list of tracks recognized by Bubble for use in Repeating Groups.
* Actions:
  * Play: Starts audio playback. This action overrides the current queue if a new source is provided.\
    &#x20;     `ID` (Text): A unique identifier for the track.\
    &#x20;     `Source` (Text): The direct URL to the audio file.\
    &#x20;     `Title` (Text): The track name shown in system controls.\
    &#x20;     `Artist` (Text): The performer name shown in system controls.\
    &#x20;     `Album` (Text): The album name.\
    &#x20;     `Genre` (Text): The music or content genre.\
    &#x20;     `Artwork URL` (Text): The URL for the cover image.\
    &#x20;     `Duration` (Number): Total length of the track in seconds.\
    &#x20;     `Extras` (JSON) (Text): Custom metadata stored as a JSON string (e.g.,  `{"key": "value"}`).\
    &#x20;     `Is Stream` (Yes/No): Set to Yes for live radio or continuous streams.\
    &#x20;     `Headers` (JSON) (Text): Custom HTTP headers for authentication/requests (e.g., `{"key": "value"}`).\
    &#x20;     `Autoplay` (Yes/No): If Yes, playback starts immediately upon loading.\
    &#x20;     `Start Position` (Number): The time (in seconds) where playback should begin.\
    &#x20;     `Volume` (Number): The initial playback volume (0.0 to 1.0).\
    &#x20;     `Speed` (Number): The initial playback rate (0.5 to 3.0).
  * Queue Add: Adds a new track to the list. Supports all parameters found in the Play action, with the addition of `Play Now` (Yes/No). If `Play Now` is checked, the player immediately switches to this track.
  * Queue Remove: Removes a specific track from the list based on its numerical position (e.g., Index 0, 1, 2).
  * Queue Get: Fetches the latest list of tracks from the native audio player to update the `Queue Track` and `Queue JSON` states.
  * Pause: Suspends playback at the current position.
  * Stop: Ends playback and resets the track position.
  * Seek: Jumps to a specific time (in seconds) within the current track.
  * Set Volume: Adjusts the player's loudness (0.0 to 1.0).
  * Set Speed: Adjusts the playback rate (0.5 to 3.0).
  * Set Metadata: Updates the information displayed on the device's lock screen and system media center without interrupting playback.\
    &#x20;     `ID` (Text): Updated unique identifier of the track.\
    &#x20;     `Title` (Text): Updated track name.\
    &#x20;     `Artist` (Text): Updated performer name.\
    &#x20;     `Album` (Text): Updated album name.\
    &#x20;     `Genre` (Text): Updated genre.\
    &#x20;     `Artwork URL` (Text): Updated URL for the cover image.\
    &#x20;     `Duration` (Number): Updated total track length.\
    &#x20;     `Extras (JSON)` (Text): Updated custom JSON metadata (e.g., `{"key": "value"}`).
    {% endtab %}

{% tab title="Javascript SDK" %}

```javascript
// ============================================================================
// NATIVELY AUDIO PLAYER - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const player = new NativelyAudioPlayer();

// ============================================================================
// ALL AVAILABLE METHODS & PARAMETERS
// ============================================================================

// player.play(sourceUrl, options, callback); 
//   - Starts playback of an audio file or stream. Overrides the current queue.
//   - sourceUrl (String): The direct URL to the audio file or stream.
//   - options (Object): Configuration and metadata.
//       * id (String): A unique identifier for the track.
//       * title (String): The track name shown in system controls.
//       * artist (String): The performer name shown in system controls.
//       * album (String): The album name.
//       * genre (String): The music or content genre.
//       * artwork (String): The URL for the cover image.
//       * duration (Number): Total length of the track in seconds.
//       * extras (Object): Custom metadata stored as a native JS object.
//       * headers (Object): Custom HTTP headers for authentication/requests.
//       * is_stream (Boolean): Set to true for live radio or continuous streams.
//       * autoplay (Boolean): If true, playback starts immediately upon loading.
//       * start_position (Number): The time (in seconds) where playback should begin.
//       * volume (Number): The initial playback volume (0.0 to 1.0).
//       * speed (Number): The initial playback rate (0.5 to 3.0).
//
// player.pause(callback); 
//   - Pauses the currently playing track.
//
// player.stop(callback); 
//   - Stops playback completely and resets the player's internal state.
//
// player.seek(positionInSeconds, callback); 
//   - Jumps to a specific timestamp in the current track.
//   - positionInSeconds (Number): The exact time in seconds.
//
// player.setVolume(volumeLevel, callback); 
//   - Adjusts the player volume.
//   - volumeLevel (Number): Accepts values from 0.0 (mute) to 1.0 (max).
//
// player.setSpeed(speedMultiplier, callback); 
//   - Changes the playback speed.
//   - speedMultiplier (Number): Accepts values like 1.0 (normal), 2.0 (double).
//
// player.queueAdd(sourceUrl, options, callback);
//   - Adds a track to the background playlist. 
//   - sourceUrl (String): The direct URL to the audio file or stream.
//   - options (Object): Accepts all metadata properties from the `play` options.
//       * play_now (Boolean): If true, interrupts current playback. If false, appends to queue.
//
// player.queueGet(callback);
//   - Returns the current state of the playlist.
//   - Callback Response Object includes:
//       * status (String): "SUCCESS" or "ERROR".
//       * queue (Array): Array of native JS objects representing queued tracks.
//       * queue_length (Number): Total number of tracks in the queue.
//       * current_index (Number): Array index (starting at 0) of the currently playing track.
//       * position (Number): Exact real-time playback position in seconds.
//
// player.queueRemove(index, callback);
//   - Removes a specific track from the queue.
//   - index (Number): The numeric index of the track in the array (e.g., 0 for the first item).
//
// player.setMetadata(metadata, callback);
//   - Updates the lock-screen/system metadata on the fly without interrupting playback.
//   - metadata (Object): Contains the updated fields.
//       * id (String): Updated unique identifier.
//       * title (String): Updated track name.
//       * artist (String): Updated performer name.
//       * album (String): Updated album name.
//       * genre (String): Updated genre.
//       * artwork (String): Updated URL for the cover image.
//       * duration (Number): Updated total track length.
//       * extras (Object): Updated custom JSON metadata.


// --- Audio Player. Quick Start (Basic Playback). Start ---

// 1. DEFINE CALLBACKS
const playbackHandler = function(resp) {
    if (resp && resp.status === "SUCCESS") {
        console.log("Playback started successfully.");
    } else {
        const errorMsg = resp ? resp.message : "Unknown playback error";
        console.error("Failed to play:", errorMsg);
    }
};

const controlHandler = function(resp) {
    console.log("Action Status:", resp ? resp.status : "Unknown");
};

// 2. CONFIGURATION (Track Details)
const audioSource = "https://example.com/audio/sample-track.mp3";
const trackOptions = {
    id: "track_123",
    title: "Awesome Podcast Episode",
    artist: "Natively",
    album: "SDK Tutorials",
    artwork: "https://example.com/images/cover.jpg",
    autoplay: true,
    volume: 1.0,
    is_stream: false // Set to true for live radio URLs
};

// 3. CORE FLOW
// Start playing a track, then pause it after a hypothetical user click
player.play(audioSource, trackOptions, playbackHandler);

// Sometime later...
// player.pause(controlHandler);
// player.stop(controlHandler);

// --- Audio Player. Quick Start (Basic Playback). End ---


// --- Advanced Usage: Queue Management & Controls. Start ---

// 1. ADD TO QUEUE
const nextTrackSource = "https://example.com/audio/next-track.mp3";
const nextTrackOptions = {
    title: "Up Next: Tech News",
    artist: "Natively",
    play_now: false // Appends to the end of the queue silently
};

player.queueAdd(nextTrackSource, nextTrackOptions, (resp) => {
    if (resp && resp.status === "SUCCESS") {
        console.log(`Track added! Queue now has ${resp.queue_length} items.`);
    } else {
        console.error("Failed to add to queue:", resp.message);
    }
});

// 2. GET QUEUE STATUS
player.queueGet((resp) => {
    if (resp && resp.status === "SUCCESS") {
        console.log("Current Queue Array:", resp.queue); // Array of track objects
        console.log("Total Tracks:", resp.queue_length);
        console.log("Currently Playing Index:", resp.current_index);
        console.log("Exact Playback Position:", resp.position); // e.g., 45.2 seconds
    } else {
        console.error("Failed to fetch queue data:", resp.message);
    }
});

// 3. REMOVE FROM QUEUE (e.g., Remove the 2nd item - index 1)
player.queueRemove(1, (resp) => {
    if (resp && resp.status === "SUCCESS") {
        console.log(`Track removed. ${resp.queue_length} items remaining.`);
    }
});

// 4. ON-THE-FLY CONTROLS (Seek & Volume)
// Jump to the 30-second mark
player.seek(30, (resp) => {
    if (resp && resp.status !== "SUCCESS") console.error("Seek failed:", resp.message);
});

// Reduce volume to 50%
player.setVolume(0.5, (resp) => console.log("Volume adjusted:", resp.status));

// --- Advanced Usage: Queue Management & Controls. End ---

```

{% endtab %}
{% endtabs %}

#### How to use

{% tabs %}
{% tab title="Bubble.io" %}
This guide covers the standard implementation of the Native Audio Player in your Bubble application, from playing a single track to building a complete queue-based playlist.

**Step 1: Place the Element on the Page**

To initialize the player, you must place the Native Audio Player element onto your Bubble page.

* This element is invisible in your live app.
* As soon as the page loads, the element automatically initializes the audio engine and exposes its default states (e.g., `Is Playing` = "no", `Queue Length` = 0).
* Important: Ensure the element is present on the page *before* triggering any workflow actions.

**Step 2: Playing a Single Track**

To immediately play a song or stream, use the Play workflow action.

* Create a workflow trigger (e.g., "When Button Play is clicked").
* Add the action: Element Actions > Play Natively-AudioPlayer.

1. Fill in the required fields:
   * Source: The secure link to your audio file (e.g., `.mp3`, `.wav`, or stream URL).
   * Artwork URL: (Optional) A link to the track's cover image.
   * Metadata: (Optional) Fill in Title, Artist, and Album to display system-level audio controls on the user's device.

**Step 3: Building a Playlist (The Queue System)**

The Native Audio Player features a robust queue system, allowing you to load multiple tracks for continuous playback.

* Queue Add: Use this action to push a new track into the player's background playlist. Set `Play Now` to "checked" if you want the track to interrupt the current audio, or "unchecked" to simply append it to the end of the line.
* Queue Get: Triggers the player to fetch the current state of the playlist. This will populate the element's `Queue Track` state (a list of Bubble objects) and update the P`osition` state.
* Queue Remove: Deletes a track from the playlist based on its numeric index.

**Step 4: Controlling Playback**

Map your custom Bubble UI (buttons, sliders) to the following element actions to control the active audio:

* Pause / Stop: Halts the current playback and automatically sets the element's `Is Playing` state to "no".
* Seek: Jumps to a specific timestamp in the audio. Pass the desired position (in seconds).
* Set Volume / Speed: Adjusts the output dynamically. Volume accepts a value from `0.0` (mute) to `1.0` (max).

**Step 5: Designing a Responsive UI (Using States)**

The plugin automatically pushes real-time data to the element's States, allowing you to build responsive UI elements without complex conditionals.

**Example:** A Play/Pause Toggle Button Instead of guessing if the audio is running, bind your UI directly to the player's data:

* Place an Icon on your page (e.g., a "Play" icon).
* Go to the Icon's Conditional tab.
* Set the condition: `When Native Audio Player A's Is Playing is yes`.
* Change the Icon to a "Pause" symbol.

**Handling Errors gracefully:** If a track fails to load or a URL is invalid, the player will change its `Status` state to `"ERROR"` and populate the `Error Message` state. You can use a Bubble "Do when condition is true" workflow (`When Native Audio Player's status is "ERROR"`) to trigger a popup or alert, notifying the user exactly what went wrong.

{% endtab %}

{% tab title="Javascript SDK" %}
Integrating the Native Audio Player into your web application is straightforward. This guide covers the standard implementation flow, from playing a single track to managing a background playlist and syncing your custom UI.

**Step 1: Initialize the Player**

Before you can play audio, you must initialize the player instance. This should typically be done once when your application or audio component loads.

```javascript
// Initialize the audio engine
const player = new NativelyAudioPlayer();
```

**Step 2: Playing a Single Track**

To immediately play a song, podcast, or live stream, use the `.play()` method. You will need the direct URL to the audio file and an options object for the system metadata (which displays on the user's lock screen).

```javascript
const audioUrl = "https://example.com/audio/sample.mp3";
const trackOptions = {
    id: "track_01",
    title: "Introduction to Natively",
    artist: "The Natively Team",
    artwork: "https://example.com/images/cover.jpg",
    autoplay: true
};

// Start playback and handle the response
player.play(audioUrl, trackOptions, (resp) => {
    if (resp.status === "SUCCESS") {
        console.log("Audio is now playing!");
    }
});
```

**Step 3: Building a Playlist (Queue System)**

If you are building an app with continuous playback, use the built-in queue system instead of manually tracking what song plays next.

Use `.queueAdd()` to push tracks into the background playlist. If the player is already running, these tracks will seamlessly play back-to-back.

```javascript
// Add a track to the end of the queue silently
player.queueAdd("https://example.com/audio/track_02.mp3", {
    title: "Chapter 1",
    play_now: false 
}, (resp) => {
    console.log(`Track added. Queue length: ${resp.queue_length}`);
});

// Interrupt current playback and play this track immediately
player.queueAdd("https://example.com/audio/breaking_news.mp3", {
    title: "Breaking News",
    play_now: true 
});
```

**Step 4: Syncing Your Custom UI**

Because audio playback is asynchronous (users can pause audio via their headphones or lock screen), you should frequently fetch the player's state to keep your custom HTML/CSS UI in sync.

Use `.queueGet()` to retrieve the exact real-time state of the player.

```javascript
function updateInterface() {
    player.queueGet((resp) => {
        if (resp.status === "SUCCESS") {
            // 1. Update your progress bar
            document.getElementById("progress").value = resp.position;
            
            // 2. Update your playlist UI
            const currentTrack = resp.queue[resp.current_index];
            document.getElementById("now-playing-title").innerText = currentTrack.title;
        }
    });
}

// Call this function periodically (e.g., via requestAnimationFrame or setInterval) 
// or whenever a user clicks a control button.
```

**Step 5: Controlling Playback**

Bind the player's control methods directly to your custom UI buttons. Always include a callback to verify the action succeeded before updating your button's visual state (e.g., switching a "Pause" icon to a "Play" icon).

```javascript
// Pause Button Handler
document.getElementById("btn-pause").addEventListener("click", () => {
    player.pause((resp) => {
        if (resp.status === "SUCCESS") {
            // Update UI to show 'Play' icon
        }
    });
});

// Seek Bar Handler (Jumping to 45 seconds)
document.getElementById("seek-bar").addEventListener("change", (e) => {
    const newTime = Number(e.target.value);
    player.seek(newTime);
});
```

**Best Practices & Error Handling**

* Sanitize URLs: Ensure all audio and artwork URLs use `https://`. Mixed content (loading `http://` audio on an `https://` site) will be blocked.
* Handle Callbacks: Always check the `resp.status` in your callbacks. If `resp.status === "ERROR"`, log or display the `resp.message` so users know why playback failed (e.g., "Network error" or "File not found").
  {% endtab %}
  {% endtabs %}

### Live Demo & Editor Example

{% tabs %}
{% tab title="Bubble.io" %}
To see a working implementation of the Native Audio Player - including playlist queues, volume controls, and real-time state bindings - we highly recommend exploring our demo application. You can test the live functionality or open the Bubble Editor to inspect the exact workflow configurations and reverse-engineer the setup for your own app.

* [View Live Demo](https://nativelyqa.bubbleapps.io/version-test/native_audio_player)
* [Inspect in Bubble Editor](https://bubble.io/page?id=nativelyqa\&test_plugin=1654595882459x381599056563798000_current\&tab=Design\&name=native_audio_player\&type=page\&elements=cmQIm0)
  {% endtab %}
  {% endtabs %}

### Troubleshooting

If the feature isn't behaving as expected, the [Debug Console](/guides/integration/debug-console) is your best friend. It reveals the conversation between your web app and the native app.

If you cannot resolve the issue using the logs, our team is here to help. To solve your issue on the first reply, we require a Standardized Bug Report based on your debug data.

Your report must include:

1. App ID: Provide the unique ID found in Natively Dashboard > Settings.
2. Actual Behavior: A clear description of what is happening (or not happening).
3. Expected Behavior: A clear description of what the app should be doing.
4. Steps to Reproduce: A list of the exact actions needed to trigger the error.
5. Console Screenshot: A capture of the Debug Console showing the specific error logs.
6. Logic Configuration: Screenshots of the specific logic where the error occurs (e.g., Bubble workflows, API connectors, or code snippets).
7. Test Credentials: If the issue requires a login to reproduce, provide a set of working test credentials (User/Pass).


# Audio Recorder

Let users record audio directly within your app using the native audio recorder.

## What is Audio Recorder?

The Audio Recorder feature gives your app access to the device's native audio recording interface, letting users record audio without leaving the app. The recording is returned as a Base64-encoded string, which you can play back or upload to your server.

Audio Recorder requests [microphone](/natively-platform/features/microphone) permission on both iOS and Android. Users will be prompted to grant this permission the first time the feature is used.

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

{% hint style="info" %}
Audio Recorder uses the [Microphone](/natively-platform/features/microphone) feature, which is enabled by default for all new apps. If the Microphone has been disabled for your app, you'll need to re-enable it for Audio Recorder to work.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Audio Recorder&#x20;

#### Events:

* **Recorder Result Updated** - called after the recording finishes.
* **Recorder Cancelled** - called when the user closes the recorder without recording.
* **File Uploaded** - called after the file is successfully uploaded to S3 (the file URL is available in the element's state).
* **File Size over limit** - triggered when the recorded file exceeds the **File Size Limit** parameter.

#### States:

* **Recorder Result (Base64)** - Base64 string representation of the recorded audio, for custom uploading.
* **Recorder Result (Content Type)** - `audio/m4a` (iOS) or `audio/wav` (Android).
* **Uploaded File URL** - Amazon S3 file URL (only populated if upload is enabled).
* **File Size** - size of the latest recording result, in KB.

#### Actions:

* **Show Recorder**
  * **Upload File** - checkbox; uploads the recording to Amazon S3.
  * **File Name** - used if **Upload File** is checked.
  * **File Size Limit** - in KB; prevents uploading files larger than this limit to Bubble's Amazon S3.
  * **Max Duration** - maximum recording duration in seconds. Leave at 0 for unlimited duration.
    {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY AUDIO RECORDER - DOCUMENTATION & EXAMPLES
// ============================================================================

const audioRecorder = new NativelyAudioRecorder();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// audioRecorder.showRecorder(max_duration, callback)
//   - Opens the native audio recorder UI and returns the result once the user finishes or cancels.
//   - max_duration: number — maximum recording duration in seconds. Pass 0 (or omit) for unlimited duration.
//   - resp.base64: string — Base64-encoded recording
//   - resp.content_type: string — "audio/m4a" (iOS) or "audio/wav" (Android)
//   - resp.size: number — file size in KB
//   - resp.status: string — "SUCCESS" or "CANCELLED"

// --- Audio Recorder. Show Recorder Example. Start ---

const max_duration = 0; // 0 = unlimited duration

const audio_recorder_callback = function (resp) {
  if (resp.status === "CANCELLED") {
    console.log("User closed the recorder without recording.");
    return;
  }

  console.log(resp.base64);        // Base64-encoded recording
  console.log(resp.content_type);  // "audio/m4a" for iOS or "audio/wav" for Android
  console.log(resp.size);          // file size in KB
};

audioRecorder.showRecorder(max_duration, audio_recorder_callback);

// --- Audio Recorder. Show Recorder Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Audio Recorder SDK: const audioRecorder = new NativelyAudioRecorder(); audioRecorder.showRecorder(max_duration, callback) opens the native recorder UI. max_duration: number — maximum recording duration in seconds, pass 0 for unlimited. Returns resp.base64 (Base64-encoded recording), resp.content_type ("audio/m4a" for iOS or "audio/wav" for Android), resp.size (file size in KB), and resp.status ("SUCCESS" or "CANCELLED" if the user closed the recorder without recording). For reference: https://docs.buildnatively.com/guides/integration/audio-recorder
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Add a voice-memo recorder to my journal app, limited to 60 seconds, that uploads the recording to my backend".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Check `resp.status` before processing the recording

A cancelled recording still triggers the callback - always confirm `resp.status === "SUCCESS"` before uploading or storing `resp.base64`.

#### Set a `max_duration` for predictable file sizes

Long recordings produce large Base64 payloads. Cap recording length if your backend has upload size limits - for example, 60 seconds for a voice memo, or a few minutes for a longer note.

#### Convert content type before playback if needed

iOS returns `audio/m4a` and Android returns `audio/wav`. If you play recordings back across platforms, confirm your player or backend handles both formats.

## Troubleshooting

<details>

<summary>Audio Recorder doesn't do anything in a web browser</summary>

This is a native feature and won't function in Safari, Chrome, or any browser preview - test it in a real build installed on a device.

</details>

<details>

<summary>Recording fails, or the user is never prompted for microphone access</summary>

Check whether the [Microphone](/natively-platform/features/microphone) feature is enabled in the Natively Dashboard. If the user previously denied microphone access, direct them to [Open App Settings](/guides/integration/open-app-settings) to re-enable it.

</details>

[^1]: Replace this placeholder


# Bottom Bar

* [Bubble.io Plugin](#bubble.io-plugin)
* [JavaScript SDK](#javascript-sdk)

### 🧋 Bubble.io Plugin

#### \[Action] Natively **- Show Bottom Bar**

This action shows the Bottom Bar and reloads the WebView.

#### \[Action] Natively - Hide Bottom Bar

This action hides the Bottom Bar and reloads the WebView.

### 🛠 JavaScript SDK

#### Show Bottom Bar

<pre class="language-javascript" data-overflow="wrap"><code class="lang-javascript"><strong>window.natively.showTabBar();
</strong></code></pre>

#### Hide Bottom Bar

```javascript
window.natively.hideTabBar();
```


# Biometrics & Credentials

Authenticate users with Face ID, Touch ID, or device passcode, and securely store and retrieve login credentials on the device.

## What is Biometrics & Credentials?

Biometrics & Credentials gives your app two related capabilities: verifying the user's identity using the device's native biometric authentication (Face ID, Touch ID, or device passcode), and securely storing and retrieving login credentials on the device.

Credentials are stored in the device's secure storage - the **iOS Keychain** on iPhone and **private Local Storage** on Android. They are tied to your app's hostname, so they are only accessible from within your specific app.

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Element] Natively - Biometrics

{% hint style="info" %}
On initialization, the element automatically checks if the user has stored credentials for your app's hostname. The result is available in the **User has stored credentials** state.
{% endhint %}

#### Events:

* **User's identity verified** - fires when biometric authentication succeeds.
* **User's identity verification failed** - fires when biometric authentication fails.
* **User's credentials received** - fires when credentials are successfully retrieved after biometric verification.
* **User's credentials not received** - fires when credentials could not be retrieved after biometric verification.
* **User's credentials saved** - fires when credentials are successfully saved after biometric verification.
* **User's credentials removed** - fires when credentials are successfully removed from the device.
* **Biometrics supported** - fires when **Check biometrics support** completes, and the device supports biometrics.
* **Biometrics not supported** - fires when **Check biometrics support** completes, and the device does not support biometrics.

#### States:

* **User's login after biometric authentication.** - the stored username/login(can be anything you're using for authentication) retrieved after successful biometric authentication.
* **User's password after biometric authentication** - the stored password retrieved after successful biometric authentication.
* **User's device supports biometrics.** - Yes/No. Whether the device supports biometric authentication.
* **User has stored credentials.** - Yes/No. Whether credentials for this app are already stored on the device.

#### Actions:

* **Check biometrics support** - checks whether the device supports biometric authentication:
  * `allow_passcode` - Yes/No. Allows users without Face ID/Touch ID to use the device passcode as a fallback.
* **Verify user's identity** - triggers native biometric authentication to confirm the user's identity:
  * `allow_passcode` - Yes/No. Allows users without Face ID/Touch ID to use the device passcode as a fallback.
* **Get user's credentials** - triggers biometric authentication and retrieves stored credentials on success:
  * `allow_passcode` - Yes/No. Allows users without Face ID/Touch ID to use the device passcode as a fallback.
* **Save user's credentials** - triggers biometric authentication and saves credentials on success:
  * `login` - the username or email to store (can be any text);
  * `password` -  the password to store (can be any text);
  * `allow_passcode` - Yes/No. Allows users without Face ID/Touch ID to use the device passcode as a fallback.
* **Remove user's credentials** - removes stored credentials from the device. No biometric authentication required.
* **Clear user's credentials from element** - clears credentials from the element state; call this after **Get user's credentials**.
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY BIOMETRICS & CREDENTIALS - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
// allowPasscode: boolean — allows users without Face ID/Touch ID to use
// the device passcode as a fallback for biometric authentication
const allowPasscode = true;
const biometrics = new NativelyBiometrics(allowPasscode);

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// biometrics.checkBiometricsSupport(callback)
//   - Checks whether the device supports biometric authentication.
//   - resp.status: true / false
//
// biometrics.checkCredentials(callback)
//   - Checks whether credentials are already stored for this app's hostname.
//   - resp.status: true / false
//
// biometrics.verifyUserIdentify(callback)
//   - Triggers native biometric authentication to verify the user's identity.
//   - Does not retrieve or save credentials.
//   - resp.status: true / false
//
// biometrics.getUserCredentials(callback)
//   - Triggers biometric authentication and retrieves stored credentials on success.
//   - resp.status: "SUCCESS_BIOMETRICS" / "FAILED_BIOMETRICS" / "FAILED_OBTAIN"
//   - resp.username: the stored username
//   - resp.password: the stored password
//
// biometrics.saveUserCredentials(username, password, callback)
//   - Triggers biometric authentication and saves the provided credentials on success.
//   - username: string — the username to store
//   - password: string — the password to store
//   - resp.status: "SUCCESS_SAVE" / "FAILED_BIOMETRICS"
//
// biometrics.removeUserCredentials(callback)
//   - Removes stored credentials from the device. No biometric authentication required.
//   - resp.status: always "success" regardless of whether credentials existed.

// --- Biometrics. Check Support Example. Start ---

const check_support_callback = function(resp) {
    if (resp.status) {
        console.log("Biometrics supported");
    } else {
        console.log("Biometrics not supported");
    }
};

biometrics.checkBiometricsSupport(check_support_callback);

// --- Biometrics. Check Support Example. End ---


// --- Biometrics. Check Credentials Example. Start ---

const check_credentials_callback = function(resp) {
    if (resp.status) {
        console.log("Credentials exist for this app");
    } else {
        console.log("No credentials stored");
    }
};

biometrics.checkCredentials(check_credentials_callback);

// --- Biometrics. Check Credentials Example. End ---


// --- Biometrics. Verify Identity Example. Start ---

const verify_identity_callback = function(resp) {
    if (resp.status) {
        console.log("Identity verified");
    } else {
        console.log("Verification failed");
    }
};

biometrics.verifyUserIdentify(verify_identity_callback);

// --- Biometrics. Verify Identity Example. End ---


// --- Biometrics. Get Credentials Example. Start ---

const get_credentials_callback = function(resp) {
    if (resp.status === "SUCCESS_BIOMETRICS") {
        console.log("Username:", resp.username);
        console.log("Password:", resp.password);
    } else {
        console.log("Failed to get credentials:", resp.status);
    }
};

biometrics.getUserCredentials(get_credentials_callback);

// --- Biometrics. Get Credentials Example. End ---


// --- Biometrics. Save Credentials Example. Start ---

const username = "user@example.com";
const password = "securepassword";

const save_credentials_callback = function(resp) {
    if (resp.status === "SUCCESS_SAVE") {
        console.log("Credentials saved successfully");
    } else {
        console.log("Failed to save credentials:", resp.status);
    }
};

biometrics.saveUserCredentials(username, password, save_credentials_callback);

// --- Biometrics. Save Credentials Example. End ---


// --- Biometrics. Remove Credentials Example. Start ---

const remove_credentials_callback = function(resp) {
    console.log("Credentials removed");
};

biometrics.removeUserCredentials(remove_credentials_callback);

// --- Biometrics. Remove Credentials Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Biometrics &#x26; Credentials SDK: const biometrics = new NativelyBiometrics(allowPasscode); // allowPasscode: boolean — true allows device passcode as fallback when Face ID/Touch ID unavailable. // biometrics.checkBiometricsSupport(callback) — resp.status: true/false. // biometrics.checkCredentials(callback) — resp.status: true/false (whether credentials exist for this app). // biometrics.verifyUserIdentify(callback) — resp.status: true/false. // biometrics.getUserCredentials(callback) — resp.status: "SUCCESS_BIOMETRICS"/"FAILED_BIOMETRICS"/"FAILED_OBTAIN". resp.username, resp.password. // biometrics.saveUserCredentials(username, password, callback) — resp.status: "SUCCESS_SAVE"/"FAILED_BIOMETRICS". // biometrics.removeUserCredentials(callback) — resp.status: always "success". For reference: https://docs.buildnatively.com/guides/integration/biometrics-and-credentials
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Add biometric login so returning users can sign in with Face ID or Touch ID instead of entering their password every time".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Check biometrics support before showing the option

Always call `checkBiometricsSupport` first to verify the device supports biometrics before showing a biometric login option. If the device doesn't support it, fall back to your standard login flow.

#### Check for stored credentials on app load

Call `checkCredentials` when the app loads to determine if the user has previously saved credentials. If credentials exist, you can offer a "Sign in with biometrics" button instead of showing the full login form.

#### Save credentials after a successful login

The right moment to save credentials is immediately after a successful standard login (email + password). Prompt the user to enable biometric login for next time - for example, with a checkbox or a prompt saying "Use Face ID for next login?". Update the stored credentials each time the user changes their password.

#### Use `verifyUserIdentity` for sensitive actions

Use `verifyUserIdentity` when you need to confirm the user's identity before a sensitive action - like viewing payment details, confirming a purchase, or accessing private data - without needing to retrieve stored credentials.

#### Always provide a fallback

Not all users have or want biometrics. Set `allowPasscode` to `true` to allow the device passcode as a fallback. Also give users the option to skip biometric login entirely and use their standard credentials instead.

#### Give users control over their credentials

Always provide a way for users to remove their stored credentials - for example, in your app's account settings. Call `removeUserCredentials` when the user signs out or disables biometric login.

#### Credentials are tied to the hostname

Stored credentials are linked to your app's hostname, not a specific user. If multiple users share a device, saving new credentials will overwrite the previously stored ones.

## Troubleshooting

<details>

<summary>Biometrics not triggering</summary>

Make sure the feature is triggered from a user interaction - like a button tap - rather than automatically on page load. Also verify the SDK is fully loaded before calling any biometric methods.

</details>

<details>

<summary><code>checkBiometricsSupport</code> returns <code>false</code></summary>

The device does not support biometric authentication, or the user has not enrolled any biometrics (no Face ID or fingerprint set up). Set `allowPasscode` to `true` to allow the device passcode as a fallback, or fall back to your standard login flow.

</details>

<details>

<summary><code>checkCredentials</code> returns <code>false</code> after saving</summary>

Credentials are tied to your app's hostname. If you're testing on a different domain or subdomain than production, the stored credentials won't be found. Make sure you're testing on the same hostname where credentials were saved.

</details>

<details>

<summary><code>getUserCredentials</code> returns <code>FAILED_OBTAIN</code></summary>

Credentials were not found for the current hostname or were removed from the device. Call `checkCredentials` first to verify credentials exist before attempting to retrieve them.

</details>

<details>

<summary>Biometric prompt not appearing on Android</summary>

The device may not have biometrics enrolled. Android requires the user to have set up fingerprint or face unlock in their device settings. If `allowPasscode` is `true`, the device PIN should appear as a fallback.

</details>

<details>

<summary>Credentials not persisting between app sessions</summary>

Make sure `saveUserCredentials` completed successfully before closing the app - check for `SUCCESS_SAVE` (iOS) or `true` (Android) in the callback. On Android, credentials are stored in private Local Storage, so clearing the app's data will remove them.

</details>

<details>

<summary>Multiple users on the same device</summary>

Credentials are tied to the hostname, not to a specific user account. If a second user saves credentials on the same device, they will overwrite the first user's stored credentials. Warn users about this if your app supports multiple accounts.

</details>

[^1]: Replace this placeholder


# Browser Info

Detect whether your app is running on iOS, Android, or a web browser, and monitor the device's network connectivity.

## What is Browser Info?

Browser Info gives your app access to two types of information: the environment it's running in, and the device's current network status. You can use it to detect whether the user is inside a Natively iOS app, a Natively Android app, or a regular web browser - and conditionally show or hide features based on that. You can also listen for connectivity changes to handle offline states gracefully.

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Browser

#### Events:

* **Device went offline** - fires when the device loses internet connectivity.
* **Device back online** - fires when the device regains internet connectivity.

#### States:

* **isAndroidApp** - Yes / No.
* **isIOSApp** - Yes / No.
* **isNativeApp** - Yes / No.
* **Connectivity** - Yes / No (Device is online or offline).
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY BROWSER INFO - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const info = new NativelyInfo();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// info.browserInfo()
//   - Returns information about the current app environment.
//   - Returns an object synchronously — no callback required.
//
// Response fields:
// browserInfo.isAndroidApp  - Boolean. true if running as a Natively Android app.
// browserInfo.isIOSApp      - Boolean. true if running as a Natively iOS app.
// browserInfo.isNativeApp   - Boolean. true if running as either a Natively iOS or Android app.

// ============================================================================
// CONNECTIVITY EVENTS
// ============================================================================
// window.ononline  - Fires when the device regains internet connectivity.
// window.onoffline - Fires when the device loses internet connectivity.

// --- Browser Info. Environment Detection Example. Start ---

const browserInfo = info.browserInfo();

console.log("Is Android app:", browserInfo.isAndroidApp);
console.log("Is iOS app:", browserInfo.isIOSApp);
console.log("Is native app:", browserInfo.isNativeApp);

// --- Browser Info. Environment Detection Example. End ---


// --- Browser Info. Connectivity Example. Start ---

window.ononline = function() {
    console.log("Device is online");
};

window.onoffline = function() {
    console.log("Device is offline");
};

// --- Browser Info. Connectivity Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Browser Info SDK: const info = new NativelyInfo(); const browserInfo = info.browserInfo(); // browserInfo.isAndroidApp — true if running as a Natively Android app. // browserInfo.isIOSApp — true if running as a Natively iOS app. // browserInfo.isNativeApp — true if running as either iOS or Android Natively app. // Connectivity: window.ononline = function() { /* device is online */ }; window.onoffline = function() { /* device is offline */ }; For reference: https://docs.buildnatively.com/guides/integration/browser-info
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Show the Sign in with Apple button only when the app is running on iOS, and hide it on Android and web browsers".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Conditional feature visibility

The most common use case is showing or hiding features based on the platform. Use `browserInfo.isIOSApp` and `browserInfo.isAndroidApp` to conditionally render platform-specific elements - for example, showing the Sign in with Apple button only on iOS, or displaying an Android-specific payment method only on Android.

#### Detecting the web context

If `browserInfo.isNativeApp` is `false`, the user is accessing your app from a regular web browser, not a Natively app. You can use this to show a prompt encouraging them to download the mobile app, or to disable features that only work in a native context.

#### Handling offline states

Use `window.ononline` and `window.onoffline` to respond to connectivity changes in real time. Common use cases include showing an offline banner, disabling form submissions when offline, or queuing actions to retry once the connection is restored.

#### Call browserInfo early

Since `info.browserInfo()` returns synchronously without a callback, call it as early as possible in your app's initialization - ideally right after the Natively SDK loads in `nativelyOnLoad()`. This ensures platform detection is available before any conditional rendering happens.

## Troubleshooting

<details>

<summary><code>browserInfo</code> values are all <code>false</code> in a web browser</summary>

This is expected behavior - `isIOSApp`, `isAndroidApp`, and `isNativeApp` are all `false` when the app is accessed from a regular web browser. Design your logic to handle this case gracefully.

</details>

<details>

<summary><code>window.ononline</code> / <code>window.onoffline</code> not firing</summary>

These are standard browser events and should work in the Natively app. If they are not firing, verify that the SDK is fully loaded before attaching the event listeners - attach them inside `nativelyOnLoad()` to ensure the environment is ready.

</details>

<details>

<summary>Platform detection runs before the SDK loads</summary>

If you call `info.browserInfo()` before the Natively SDK has finished loading, the values may be incorrect. Always call it inside `nativelyOnLoad()` or after confirming the SDK is initialized.

</details>

[^1]: Replace this placeholder


# Clipboard

Copy text to and read text from the device clipboard within your app.

## What is Clipboard?

The Clipboard feature lets your app interact with the device clipboard - copying text programmatically or reading whatever the user has previously copied. This is useful for one-tap copy actions like referral codes, wallet addresses, tracking numbers, or any text your users might want to share or reuse elsewhere.

{% hint style="warning" %}
Only plain text is supported. Copying images or other media is not available.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### &#x20;Setup logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Clipboard

#### Events:

* **Clipboard value received** - fires when a text value is successfully retrieved from the clipboard.

#### States:

* **Latest clipboard value** - the text currently stored in the device clipboard.

#### Actions:

* **Copy text to clipboard** - copies the provided text to the device clipboard:
  * `text` - the plain text string to copy.
* **Get text from the clipboard** - reads the current text value from the device clipboard. Listen to the **Clipboard value received** event to access the result.
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY CLIPBOARD - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const clipboard = new NativelyClipboard();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// clipboard.copy(text)
//   - Copies the provided text to the device clipboard.
//   - text: string — the text to copy. Only plain text is supported.
//
// clipboard.paste(callback)
//   - Reads the current text value from the device clipboard.
//   - Returns the text via callback.

// ============================================================================
// CALLBACK RESPONSE FIELDS
// ============================================================================
// resp.text  - The text currently stored in the device clipboard.

// --- Clipboard. Copy Example. Start ---

clipboard.copy("Your text here");

// --- Clipboard. Copy Example. End ---


// --- Clipboard. Paste Example. Start ---

const clipboard_callback = function(resp) {
    console.log(resp.text); // the text currently in the clipboard
};

clipboard.paste(clipboard_callback);

// --- Clipboard. Paste Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively JS SDK: const clipboard = new NativelyClipboard(); // clipboard.copy(text) — copies plain text string to device clipboard. text: string — the text to copy. No callback required. // clipboard.paste(callback) — reads current text from device clipboard. Returns resp.text in callback. // resp.text — the text currently stored in the device clipboard. // Example copy: clipboard.copy("Your text here"); // Example paste: const clipboard_callback = function(resp) { console.log(resp.text); }; clipboard.paste(clipboard_callback); For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://docs.buildnatively.com/guides/integration/clipboard
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Add a copy button next to the referral code that shows a confirmation message when tapped".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### **One-tap copy**

The most common use case is a copy button next to a piece of text - a referral code, promo code, wallet address, or invite link. Trigger `clipboard.copy()` on button click and optionally show a [Toast/Banner](/guides/integration/toast-banner) confirmation so the user knows the copy was successful.

#### **Reading clipboard on app open**

You can read the clipboard when a page loads to pre-fill a form field - for example, if a user copied a promo code from another app before opening yours. Use `clipboard.paste()` on page load, and populate the relevant input field with the result.

#### **No permission required**

Unlike some other device features, the Clipboard does not require any permission request on iOS or Android. It works silently in the background.

## Troubleshooting

<details>

<summary>Paste method returns empty text</summary>

The clipboard may be empty, or the user hasn't copied anything yet. Always handle the case where `resp.text` is empty or null in your callback before using the value.

</details>

<details>

<summary>Copied text not appearing in another app</summary>

Verify that `clipboard.copy()` is being triggered correctly by [logging](/natively-platform/features/deep-links) the text value before passing it to the method. Ensure the text is a plain string - objects or arrays are not supported.

</details>

<details>

<summary>Clipboard not working in Preview</summary>

The Clipboard feature works in the Natively Preview app. If you experience issues, try triggering a full build and testing on a real device.

</details>

[^1]: Replace this placeholder


# Control Style & Colors

Styles and colors in your native mobile app enhance branding, user experience, accessibility, personalization, adaptability, provide a competitive edge while offering flexibility for future updates.

* [Bubble.io Plugin](#bubble.io-plugin)
* [JavaScript SDK](#javascript-sdk)

### 🧋 Bubble.io Plugin

#### \[Action] Natively - Background Color

Updating the [App background-color](broken://pages/eaCQDdq9F0TqdxBbYvz0#app-background-color)

#### \[Action] Natively - Progress Color

Updating a [progress bar color](broken://pages/eaCQDdq9F0TqdxBbYvz0#loader-color)

#### \[Action] Natively - Swipe Navigation

Enable or disable [Swipe Navigation](broken://pages/1VcIjqjCzM2xGnmpHHRO#swipe-navigation)

#### \[Action] Natively - Status Bar Style

Updating the [Status Bar Style](broken://pages/1VcIjqjCzM2xGnmpHHRO#status-bar-style-ios)

#### \[Action] Natively - Pull to refresh

Enable or disable [Pull to refresh](broken://pages/1VcIjqjCzM2xGnmpHHRO#pull-to-refresh)

#### \[Action] Natively - Orientation

Change and lock device orientation in the app

#### \[Action] Natively - Show Progress

Hide / show progress in the app

#### \[Action] Natively - Close App

Closes (kill) the app

#### \[Action] Natively - Enable Wake Lock

Keeps the screen unlocked

#### \[Action] Natively - DIsable Wake Lock

Allows screen to auto lock

### 🛠 JavaScript SDK

#### Updating the [App background-color](broken://pages/eaCQDdq9F0TqdxBbYvz0#app-background-color)

{% code overflow="wrap" lineNumbers="true" %}

```javascript
const color = "#000000";
window.natively.setAppBackgroundColor(color);
```

{% endcode %}

#### Updating a [progress bar color](broken://pages/eaCQDdq9F0TqdxBbYvz0#loader-color)

{% code lineNumbers="true" %}

```javascript
const color = "#000000";
window.natively.setAppProgressColor(color);
```

{% endcode %}

#### Enable or disable [Swipe Navigation](broken://pages/1VcIjqjCzM2xGnmpHHRO#swipe-navigation)

{% code lineNumbers="true" %}

```javascript
const toggle = true;
window.natively.setAppSwipeNavigationIOS(toggle);
```

{% endcode %}

#### Updating the [Status Bar Style](broken://pages/1VcIjqjCzM2xGnmpHHRO#status-bar-style-ios)

{% code lineNumbers="true" %}

```javascript
const style = "NONE"; // DARK, LIGHT or NONE
window.natively.setAppStatusBarStyleIOS(toggle);
```

{% endcode %}

#### Enable or disable [Pull to refresh](broken://pages/1VcIjqjCzM2xGnmpHHRO#pull-to-refresh)

{% code lineNumbers="true" %}

```javascript
const toggle = true;
window.natively.setAppPullToRefresh(toggle);
```

{% endcode %}

#### Change and lock device orientation in the app

{% code lineNumbers="true" %}

```javascript
const orientation = "PORTRAIT"; // DEFAULT, PORTRAIT or LANDSCAPE
window.natively.setAppOrientation(orientation);
```

{% endcode %}

#### Show/Hide Progress in the app

{% code lineNumbers="true" %}

```javascript
const toggle = false;
window.natively.showProgress(toggle);
```

{% endcode %}

#### Close the app

{% code lineNumbers="true" %}

```javascript
window.natively.closeApp()
```

{% endcode %}

#### Wake Lock

```javascript
window.natively.enableWakelock(); // Keeps the screen unlocked
and window.natively.disableWakelock(); // Allows screen to auto lock
```


# Date Picker

Display a native date, time, or date and time picker in your app.

## What is Date Picker?

The Date Picker feature lets you show a native date and time selection UI - the same picker the user sees in other apps on their device. It supports three modes: date only, time only, or date and time combined. The result is returned as a timestamp in milliseconds, which you can convert to any date format your app needs.

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Element] Natively - Date Picker

#### Events:

* Date selected
* Date Picker closed

#### States:

* Selected Date

#### Actions:

* Show Date Picker
  * **Title** - label shown at the top of the picker.
  * **Description** - optional subtitle shown below the title (iOS only - may not have a visible effect on Android).
  * **Type** - `DATE`, `TIME`, or `DATE_AND_TIME` .
  * **Style** - `DARK` or `LIGHT` .
    {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY DATE PICKER - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const picker = new NativelyDatePicker();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// picker.showDatePicker(title, description, type, style, callback)
//   - Displays the native date/time picker.
//   - title: string — label shown at the top of the picker
//   - description: string — optional subtitle shown below the title (iOS only - may not have a visible effect on Android)
//   - type: string — "DATE" / "TIME" / "DATE_AND_TIME"
//   - style: string — "LIGHT" / "DARK"
//   - callback: function — called when the user selects a date or dismisses the picker

// ============================================================================
// CALLBACK RESPONSE FIELDS
// ============================================================================
// resp.date  - Milliseconds since epoch (Unix timestamp in ms).
//              Convert to a Date object: new Date(Number(resp.date))
//              Returns an invalid date if the user dismisses without selecting.

// --- Date Picker. Example. Start ---

const title = "Select Date";
const description = "";
const type = "DATE_AND_TIME"; // "DATE" / "TIME" / "DATE_AND_TIME"
const style = "LIGHT"; // "LIGHT" / "DARK"

const datepicker_callback = function(resp) {
    const date = new Date(Number(resp.date));

    if (isNaN(date.getTime())) {
        // User dismissed the picker without selecting a date
        console.log("No date selected");
        return;
    }

    console.log("Selected date:", date);
};

picker.showDatePicker(title, description, type, style, datepicker_callback);

// --- Date Picker. Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Date Picker SDK: const picker = new NativelyDatePicker(); // picker.showDatePicker(title, description, type, style, callback) — shows the native date/time picker. // title: string — label shown at the top of the picker. // description: string — optional subtitle shown below the title (iOS only, may not affect Android). // type: "DATE" / "TIME" / "DATE_AND_TIME". // style: "LIGHT" / "DARK". // resp.date — milliseconds since epoch. Convert with: new Date(Number(resp.date)). Returns invalid date if user dismisses without selecting. For reference: https://docs.buildnatively.com/guides/integration/date-picker
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Show a native date picker when the user taps the booking date field and save the selected date to the form".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Choose the right type

Use `DATE` for date-only inputs like birthdays or booking dates, `TIME` for scheduling or time-based inputs, and `DATE_AND_TIME` when you need both - for example, scheduling a meeting or setting a reminder.

#### Converting the timestamp

The picker returns milliseconds since epoch via `resp.date`. Convert it to a JavaScript Date object with `new Date(Number(resp.date))`. From there, you can format it however your app needs - as a readable string, store it in your database, or pass it to another function.

#### Handling dismissal

If the user closes the picker without selecting a date, `resp.date` will return an invalid date. Always check with `isNaN(date.getTime())` before using the value to avoid passing invalid data to your app logic.

#### Match the picker style to your app

Use `LIGHT` or `DARK` to match your app's visual theme. If your app supports both light and dark modes, consider reading the current mode from [Device Info](/guides/integration/device-info) and passing it dynamically to the picker.

## Troubleshooting

<details>

<summary>The selected date is invalid or <code>NaN</code></summary>

The user dismissed the picker without making a selection. This is expected behavior - always validate the returned value with `isNaN(date.getTime())` before using it in your app logic.

</details>

<details>

<summary>Description not visible on Android</summary>

The description field may only have a visible effect on iOS. If you need to display additional context on Android, consider adding it as a label in your app's UI instead.

</details>

<details>

<summary>Wrong date format returned</summary>

The picker always returns milliseconds since epoch - not a formatted date string. Use `new Date(Number(resp.date))` to convert it, then apply your preferred formatting library or method.

</details>

<details>

<summary>Date picker not appearing</summary>

Make sure the Natively SDK is fully loaded before calling `picker.showDatePicker()`. Trigger it via a user interaction after the SDK has initialized.

</details>

<details>

<summary>Style not applying correctly</summary>

Make sure the `style` parameter is either `"LIGHT"` or `"DARK"` - any other value may cause unexpected behavior.

</details>

[^1]: Replace this placeholder


# Debug Console

Inspect native feature communication and monitor logs directly on your device.

## What is the Debug Console?

The Debug Console is a built-in inspector that slides up over your app, letting you monitor the communication between your web app and the native mobile environment in real time. It's the most powerful tool for diagnosing issues with native features - you can see exactly what data is being sent and received between your app and native SDKs like OneSignal or RevenueCat.

{% hint style="info" %}
When submitting a support ticket about a native feature not working, always attach a screenshot of the Debug Console. It gives our team the raw logs needed to diagnose the issue immediately.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), **NPM Package** (React/Next.js), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="npm Package" %}
**Check npm Package**

Before writing any logic, ensure the Natively NPM package is installed and up to date in your project.

```bash
npm install natively@latest
```

{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Action] Natively - Open Debug Console

Opens the Debug Console overlay. No parameters required. Add this action to any workflow - for example, when a button is clicked.

<figure><img src="/files/05PkiOXue6fBl9bR2M0s" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY DEBUG CONSOLE - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — openConsole is available directly
// on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.openConsole()
//   - Opens the Debug Console overlay.
//   - No parameters required. No callback.

// --- Debug Console. Example. Start ---

window.natively.openConsole();

// --- Debug Console. Example. End ---
```

{% endtab %}

{% tab title="npm Package" %}

```javascript
'use client';

import { useNatively } from 'natively';

export default function DebugButton() {
  const natively = useNatively();

  return (
    <button onClick={() => natively.openConsole()}>
      Open Debug Console
    </button>
  );
}
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Debug Console SDK: // window.natively.openConsole() — opens the Debug Console overlay. No parameters or callback required. For reference: https://docs.buildnatively.com/guides/integration/debug-console
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Add a hidden debug button that opens the Natively Debug Console when tapped 3 times".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Trigger from a button, not on page load

The most common pattern is adding a hidden button somewhere in your app - for example, in a developer settings screen or triggered by tapping a logo multiple times - that opens the console on demand. Avoid triggering it automatically on page load.

#### Check if running inside the native app first

The Debug Console only works inside a Natively app. Use Browser Info to check `browserInfo.isNativeApp` before calling `openConsole()`, so you don't trigger it accidentally in a web browser.

#### Monitor native SDK communication

Open the console and trigger the feature you're debugging - push notifications, in-app purchases, AdMob - then watch the logs appear in real time. You can see exactly what data your web app is sending to the native layer and what's coming back.

#### Attach a screenshot to support tickets

When something isn't working, and you're contacting Natively support, always include a screenshot of the Debug Console showing the relevant logs. This allows the support team to skip the diagnosis phase and go straight to a fix.

#### Use setDebug(true) during development

Setting `window.natively.setDebug(true)` in your SDK initialization shows native OS alerts when an SDK error occurs, giving you an additional layer of visibility during development. Set it to `false` before releasing to production.

## Troubleshooting

<details>

<summary>Nothing happens when triggering the Debug Console</summary>

Two common causes:

* **Not running inside a Natively app** - the Debug Console is a native feature and will not work in a standard mobile browser (Safari, Chrome) or wrappers created by other services. Test on a real device using a Natively build or the Preview app.
* **SDK not correctly installed** - the `openConsole()` command is a message sent from your website to the native app. If the Natively SDK isn't properly installed in your web app's `<head>`, the message is never sent. To verify, open your browser's inspector on desktop and type `window.natively` - if it returns `undefined`, the SDK is not integrated correctly.

</details>

<details>

<summary>The console opens but shows no logs for a specific feature</summary>

Make sure you trigger the feature after the page has loaded. The console shows logs accumulated since page load - if you triggered the feature before the page fully loaded, those logs may not appear. Try refreshing the page and triggering the feature again before opening the console.

</details>

<details>

<summary>Debug Console is not available in a web browser</summary>

This is expected - the Debug Console is a native feature. Use your browser's built-in developer tools (`console.log`) for web-side debugging instead.

</details>

[^1]: Replace this placeholder


# Device Info

Access details about the user's device, app, and runtime environment.

## What is Device Info?

Device Info gives your app access to details about the user's device and your app's runtime environment - device model, OS version, app build info, dark mode status, and screen orientation. It also lets you listen for app state changes (foreground/background) and keyboard visibility, and includes an error handler for managing HTTP request failures without showing the default error screen.

{% hint style="info" %}
For platform detection (iOS, Android, or web), see [Browser Info](/guides/integration/browser-info) instead.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Element] Natively - Device

{% hint style="info" %}
**Refresh App Info** action is called automatically on element load, so device information is available as soon as the element initializes. Avoid adding multiple **Natively - Device** elements on the same page, as each one sends a request to the device on load.
{% endhint %}

#### **Events:**

* **App Info received** - fires when device and app information is successfully retrieved.
* **App went foreground** - fires when the user opens the app.
* **App went background** - fires when the user minimizes the app without exiting.
* **Keyboard is visible** - fires when the on-screen keyboard appears.
* **Keyboard is hidden** - fires when the on-screen keyboard is dismissed.
* **Error occurred** - fires when an HTTP request fails, and the **Set Error Handler** action has been previously called on the current page. (Available starting from v. 2.26.&#x30;**)**

#### **States:**

* **Device** - device model identifier. (iOS values can be decoded with this [list](https://github.com/pbakondy/ios-device-list/blob/master/devices.json))
* **OS Version** - Android / iOS version.
* **App Version** - your app's build version.
* **Build Number** - your app's build number.
* **Natively SDK Version** - the Natively SDK version in use.
* **OS Name** - "Android" or "iOS".
* **isDarkMode** - Yes / No.
* **Orientation** - "PORTRAIT" or "LANDSCAPE".
* **Error Code** - the HTTP status code of the failed request (e.g. 404, 500).
* **Error Description** - a text description of the error provided by the requester endpoint.
* **Error Response Data** - the raw response body (JSON/Text) returned by the server for the failed HTTP request (if available).

#### Actions:

* **Refresh App Info** - retrieves the latest device and app information.
* **Set Error Handler** - enables custom error handling for the current page session. When active, the app will not automatically display the native error screen when an HTTP request fails. Instead, it fires the **Error occurred** event.

#### How to use the Error Handler

To ensure consistent error handling across your app, place the logic in a Reusable Element (like your Header or a dedicated technical Reusable) that exists on every page.

1. Place the **Natively - Device** element inside your Reusable Element.
2. When Natively - Device's Device state is not empty (this ensures the element is fully initialized) > call the **Set Error Handler** action.\
   ![](/files/8NPvX56w8P2SrMlXNAPo)
3. Create a workflow to handle the error when it happens (the event **Error occurred** is fired).\
   ![](/files/tKx9Uxt6VXsy3Gjd3qmO)
4. The error's details can also be visible in the Debug console.
   {% endtab %}

{% tab title="JavaScript SDK" %}

#### Device Info

```javascript
// ============================================================================
// NATIVELY DEVICE INFO - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const info = new NativelyInfo();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// info.getAppInfo(callback)
//   - Retrieves device, OS, and app build information.
//   - Returns the result via callback.
//
// info.app_state(callback)
//   - Listens for app state changes (foreground/background).
//
// info.keyboard_visibility(callback)
//   - Listens for changes in on-screen keyboard visibility.

// ============================================================================
// CALLBACK RESPONSE FIELDS - getAppInfo
// ============================================================================
// resp.device        - Device model (iOS values can be decoded with this list:
//                       https://github.com/pbakondy/ios-device-list/blob/master/devices.json)
// resp.osVersion     - Android/iOS version.
// resp.osName        - "iOS" or "Android".
// resp.buildVersion  - Your app's build version.
// resp.buildNumber   - Your app's build number.
// resp.sdkVersion    - The Natively SDK version in use.
// resp.isDarkMode    - Boolean. true if the device is in dark mode.
// resp.orientation   - "PORTRAIT" or "LANDSCAPE".

// ============================================================================
// CALLBACK RESPONSE FIELDS - app_state
// ============================================================================
// resp.state   - Boolean. true if the app is in the foreground, false if backgrounded.

// ============================================================================
// CALLBACK RESPONSE FIELDS - keyboard_visibility
// ============================================================================
// resp.visible - Boolean. true if the on-screen keyboard is visible, false if hidden.


// --- Device Info. Get App Info Example. Start ---

const app_info_callback = function(resp) {
    console.log(resp.device);       // iPhone14,2
    console.log(resp.osVersion);    // 15.6
    console.log(resp.osName);       // iOS / Android
    console.log(resp.buildVersion); // 1.0.0
    console.log(resp.buildNumber);  // 1
    console.log(resp.sdkVersion);   // 3
    console.log(resp.isDarkMode);   // true/false
    console.log(resp.orientation);  // PORTRAIT / LANDSCAPE
};

info.getAppInfo(app_info_callback);

// --- Device Info. Get App Info Example. End ---


// --- Device Info. App State Example. Start ---

const app_state_callback = function(resp) {
    if (resp.state) {
        console.log("App went foreground");
    } else {
        console.log("App went background");
    }
};

info.app_state(app_state_callback);

// --- Device Info. App State Example. End ---


// --- Device Info. Keyboard Visibility Example. Start ---

const keyboard_visibility_callback = function(resp) {
    if (resp.visible) {
        console.log("Keyboard is visible");
    } else {
        console.log("Keyboard is hidden");
    }
};

info.keyboard_visibility(keyboard_visibility_callback);

// --- Device Info. Keyboard Visibility Example. End ---

```

#### Error Handler

```javascript
// ============================================================================
// NATIVELY ERROR HANDLER - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — setErrorHandler is available directly
// on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.setErrorHandler(callback)
//   - Suppresses the default native error screen on failed HTTP requests.
//   - Fires the callback instead, letting you handle the error in your own workflow.

// ============================================================================
// CALLBACK RESPONSE FIELDS
// ============================================================================
// resp.code          - HTTP status code of the failed request (e.g. 404, 500, 0).
// resp.description   - Text description of the error (e.g. "Network Request Failed", "Not Found").
// resp.url           - The URL of the failed request.
// resp.type          - "NETWORK_ERROR" or "HTTP_ERROR".
// resp.response_data - The raw response body (usually an Object — stringify it to read it).

// --- Error Handler. Example. Start ---

const error_handler_callback = function(resp) {
    console.log(resp.code);
    console.log(resp.description);
    console.log(resp.url);
    console.log(resp.type);
    console.log(JSON.stringify(resp.response_data));
};

window.natively.setErrorHandler(error_handler_callback);

// --- Error Handler. Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the relevant line below and paste it into your AI agent.

#### Device Info

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Device Info SDK: const info = new NativelyInfo(); // info.getAppInfo(callback) — retrieves device info. resp.device: device model. resp.osVersion: OS version. resp.osName: "iOS" or "Android". resp.buildVersion: app version. resp.buildNumber: build number. resp.sdkVersion: SDK version. resp.isDarkMode: boolean. resp.orientation: "PORTRAIT" or "LANDSCAPE". // info.app_state(callback) — listens for app state changes. resp.state: boolean (true=foreground, false=background). // info.keyboard_visibility(callback) — listens for on-screen keyboard visibility changes. resp.visible: boolean (true=visible, false=hidden). For reference: https://docs.buildnatively.com/guides/integration/device-info
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "When the app loads, get the device info and display the OS name and app version on the settings page".
{% endhint %}

#### Error Handler

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Error Handler: // window.natively.setErrorHandler(callback) — suppresses default error screen on HTTP failures. No initialization required. resp.code: HTTP status code. resp.description: error description. resp.url: failed URL. resp.type: "NETWORK_ERROR" or "HTTP_ERROR". resp.response_data: raw response body (stringify to read). For reference: https://docs.buildnatively.com/guides/integration/device-info
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Handle HTTP errors silently without showing the native error screen, and display a custom error message instead".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Reading device info on app load

Call `info.getAppInfo()` early in your app's initialization, ideally inside `nativelyOnLoad(),` so device details are available before any conditional logic runs. Store the result rather than calling it repeatedly.

#### Detect dark mode

Use `resp.isDarkMode` to apply the correct theme when the app loads. Combine with `app_state` to re-check when the user returns to the foreground, since they may have changed their system appearance while the app was backgrounded.

#### Handle orientation changes

Use `resp.orientation` from `getAppInfo()` to adjust your layout on load. Note that this is a snapshot at the time of the call - it won't update automatically as the device rotates. Call `getAppInfo()` again inside `app_state` if you need to track rotation.

#### App state: foreground and background

Use `resp.state` inside `app_state` to pause or resume activity when the user switches apps. Common use cases include pausing media playback, stopping timers, or refreshing data when the user returns.

#### Keyboard visibility

Use `resp.visible` inside `keyboard_visibility` to adjust your layout when the keyboard appears or disappears - for example, scrolling to a focused input or hiding a bottom navigation bar.

#### Error Handler - when to use it

Enable the Error Handler when you want to handle HTTP failures gracefully instead of showing the native error screen. Place it in a Reusable Element that exists on every page so it's always active. Use `resp.type` to distinguish between network failures (`NETWORK_ERROR`) and server errors (`HTTP_ERROR`), and `resp.code` to respond differently to specific status codes like 401 or 500.

## Troubleshooting

<details>

<summary><code>getAppInfo()</code> returns empty or undefined values</summary>

Make sure the call is made after the SDK has fully loaded. Call it inside `nativelyOnLoad()` or trigger it from a user interaction rather than immediately on page load.

</details>

<details>

<summary><code>app_state</code> is not firing</summary>

The `app_state` listener needs to be registered after the SDK is loaded. Register it inside `nativelyOnLoad()` to ensure it's active before any state changes occur.

</details>

<details>

<summary>Orientation is not updating on device rotation</summary>

`getAppInfo()` returns a snapshot of the orientation at the time of the call - it does not update automatically. Call `getAppInfo()` again inside your `app_state` callback when the app returns to the foreground to get the latest orientation value.

</details>

<details>

<summary>Keyboard visibility events are not firing</summary>

Keyboard visibility is reported through the separate `keyboard_visibility` method, not `app_state`. Make sure you've registered a callback with `info.keyboard_visibility(callback)` and that it's called after the SDK has loaded.

</details>

<details>

<summary>Error Handler is not suppressing the native error screen</summary>

Make sure `window.natively.setErrorHandler()` is called before any HTTP requests are made. Place it in a Reusable Element that loads on every page so it's always active. If it's only called on specific pages, the error screen will still appear on pages where it hasn't been set.

</details>

<details>

<summary><code>resp.response_data</code> is not readable</summary>

`resp.response_data` is usually an Object - use `JSON.stringify(resp.response_data)` to convert it to a readable string before logging or displaying it.

</details>

[^1]: Replace this placeholder


# Geolocation

* [Bubble.io Plugin](#bubble.io-plugin)
* [JavaScript SDK](#javascript-sdk)

{% hint style="info" %}
If you're planning to use browser [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/geolocation), make sure you've enabled the Geolocation feature for iOS (For Android, this feature is enabled by default)
{% endhint %}

{% hint style="info" %}
To avoid system conflicts and battery drain, follow these simple rules:

* Need tracking only when the app is open? Use the `Foreground Geolocation` feature only.
* Need tracking even when the app is closed? Use the `Background Geolocation` feature only. The background service automatically handles foreground tracking; you do not need to run both simultaneously.

Warning: Running both services concurrently will lead to unpredictable behavior and rapid battery depletion. Always stop one before starting the other.
{% endhint %}

## 🧋 Bubble.io Plugin

### \[Element] Natively - Location

**Natively - Location** element contains Foreground & Background location services:&#x20;

#### 📍 Foreground Location Tracking

#### Events:

* **Location received** - **User's location longitude & latitude** values updated.
* **Location failed** - called whenever Location service was not able to get user's location (check **Latest location request status** for more details)
* **Location Permission Received -** called whenever **Get location permission** return a result
* **v2.13.4 - Location Background Service Status Received** - called afrer **Get background location service status**

#### States:

* **Latest location longitude** - [Longitude](https://en.wikipedia.org/wiki/Longitude) as number
* **Latest location latitude** - [Latitude](https://en.wikipedia.org/wiki/Latitude) as number
* **Latest location response status** - Status of the latest geolocation request call.
  * SUCCESS - Got a location and desired accuracy level was achieved successfully.
  * TIMEOUT - Got a location, but the desired accuracy level was not reached before timeout.
  * FAILED - An error occurred while using the system location services.
* **Location permission**
  * IN\_USE - foreground allowed
  * ALWAYS - background allowed
  * DENIED - not determined or denied
* **v2.13.4 -BG Location Is Running** - yes/no (will be changed on Stop/Start Background Location or **Get background location service status**)

#### Actions:

* **Get current location**
  * minAccuracy \[iOS] - Radius in meters. All other values will be filtered!
  * accuractType \[iOS] - Affects on location accuracy and battery life. BestForNavigation,Best,NearestTenMeters,HundredMeters,Kilometer,ThreeKilometers. More details [here](https://developer.apple.com/documentation/corelocation/cllocationaccuracy)
  * Priority \[Android] - "BALANCED" (To save battery, recommended) & "HIGH"
  * Open Settings - If enabled and user's Location permission is denied, will display a pop-up with the option to open the app settings and turn on permission. - v2.18.0
* **Start foreground location service** - start fetching device location&#x20;
  * minAccuracy \[iOS] - Radius in meters. All other values will be filtered!
  * accuractType \[iOS] - Affects on location accuracy and battery life. BestForNavigation,Best,NearestTenMeters,HundredMeters,Kilometer,ThreeKilometers. More details [here](https://developer.apple.com/documentation/corelocation/cllocationaccuracy)
  * Priority \[Android] - "BALANCED" (To save battery, recommended) & "HIGH"
  * Fetching Interval - Determines how often the location will be fetched. Interval in milliseconds. 5000ms -> 5s
  * Open Settings - If enabled and user's Location permission is denied, will display a pop-up with the option to open the app settings and turn on permission. - v2.18.0
* **Stop foreground location service** - stop fetching device location
* **Get location permission** - request location permission status update
* **v2.13.4 -Get background location service status** - check if BG Location Service already running

{% hint style="info" %}
On page reload, the foreground location tracking service will be automatically stopped. You need to run the **Start foreground location service** action.
{% endhint %}

#### 🚙 Background location tracking - v2.1.0

{% hint style="info" %}
Before using background geolocation, make sure you've enabled it in your Natively App Dashboard + Created a new build.
{% endhint %}

#### States:

* **Latest location response status** - Status of the latest background location service start/stop.
  * SUCCESS - Background location started/stopped successfully
  * FAILED - Background location started/stopped with an error.

#### Events:

* **Location background failed** - Whenever starting of background service will fail
* **Location background success** -  Whenever starting of background service will succeed

#### Actions:

* **Start background location service** - start fetching device location&#x20;
  * minAccuracy \[iOS] - Radius in meters. All other values will be filtered!
  * accuractType \[iOS] - Affects on location accuracy and battery life. BestForNavigation,Best,NearestTenMeters,HundredMeters,Kilometer,ThreeKilometers. More details [here](https://developer.apple.com/documentation/corelocation/cllocationaccuracy)
  * Priority \[Android] - "BALANCED" (To save battery, recommended) & "HIGH"
  * Fetching Interval - Determines how often the location will be fetched. Interval in milliseconds. 5000ms -> 5s
  * Response Identifier - Identifier that will be sent with a user's location (We're recommending using the user's unique id)
  * Open Settings - If enabled and user's Location permission is denied, will display a pop-up with the option to open the app settings and turn on permission. - v2.18.0
* **Stop background location service** - stop fetching device location

{% hint style="warning" %}
Background geolocation can be automatically stopped in such cases: If the user or system closes the app,  or if the device receives more than 3 errors in response from your endpoint.
{% endhint %}

{% hint style="info" %}
Unlike foreground location service, the background will not stop on page reload
{% endhint %}

### Coordinates -> Marker address

To convert longitude and latitude to the bubble's geolocation, you need to use this formula:

![](/files/K2FMKyY2su5jinGSE7MR)

## 🛠 JavaScript SDK

### NativelyLocation

{% code overflow="wrap" lineNumbers="true" %}

```javascript
const locationService = new NativelyLocation()
const location_callback = function (resp) {
        console.log(resp.status); // "Success"/"Timeout"/"Error"
        console.log(resp.longitude); // 50.1231231
        console.log(resp.latitude); // 50.1231231
};
const minAccuracy = 50 // only available in 2.7.0 minAccuracy [iOS] - Radius in meters. All other values will be filtered!

const accuracyType = "Best" // only available in 2.7.0 accuractType [iOS] - Affects on location accuracy and battery life. BestForNavigation,Best,NearestTenMeters,HundredMeters,Kilometer,ThreeKilometers. More details here https://developer.apple.com/documentation/corelocation/cllocationaccuracy

const priority_android = "BALANCED" // "BALANCED" or "HIGH" [Android only]
const inerval = 10000 // in milliseconds 
const fallback_to_settings = true; // available from v2.15.16. If true and user's Location permission is denied, will display a pop-up with the option to open the app settings and turn on permission.

// Foreground Location
locationService.current(minAccuracy, accuracyType, priority_android, location_callback, fallback_to_settings);
locationService.start(interval, minAccuracy, accuracyType, priority_android, location_callback, fallback_to_settings);
locationService.stop();

const location_bg_callback = function (resp) {
        console.log(resp.status); // "SUCCESS"/"FAILED" - Start of background service was successfully or failed
};
const location_status_bg_callback = function (resp) {
        console.log(resp.status); // true/false - BG Location Service running or not
};
// Background Location
const responseId = "my-user-id" // SHOULD BE A STRING! Identifer that you will receive together with coordinates to your webhook endpoint
locationService.startBackground(interval, minAccuracy, accuracyType, priority_android, responseId, location_bg_callback, fallback_to_settings);
locationService.stopBackground(location_bg_callback);
locationService.statusBackground(location_status_bg_callback); // >=v2.12.3
```

{% endcode %}


# Haptic Feedback

Add tactile vibration responses to interactions in your app using native haptic feedback.

## What is Haptic Feedback?

Haptic Feedback lets your app communicate with users through touch - using the device's vibration motor to provide subtle physical responses to interactions. This makes your app feel more responsive and polished, reinforcing actions such as button taps, form submissions, errors, and custom sequences without relying solely on visual or audio cues.

Natively supports three types of haptic feedback: **Impact** for simple tactile responses, **Notification** for status-based feedback (success, error, warning), and **Pattern** for custom vibration sequences.

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### **\[Action] Natively - Haptic Impact**

* **Type** - `LIGHT`, `MEDIUM`, `HEAVY`, `RIGID`, or `SOFT` .

#### **\[Action] Natively - Haptic Notification**

* **Type** - `SUCCESS`, `ERROR`, or `WARNING` .

#### **\[Action] Natively - Haptic Pattern**

* **Pattern** — a string of symbols representing a custom vibration sequence:
  * `O` - heavy impact;
  * `o` - medium impact;
  * `.` - light impact;
  * `X` - rigid impact;
  * `x` - soft impact;
  * `-` - wait 0.1 second;
* **Delay** — time between each element in the pattern (must be `>= 0` and `< 1`).
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY HAPTIC FEEDBACK - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required - haptic methods are available directly
// on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.hapticImpact(type)
//   - Triggers a single haptic impact.
//   - type: string - "LIGHT" / "MEDIUM" / "HEAVY" / "RIGID" / "SOFT"
//
// window.natively.hapticNotification(type)
//   - Triggers a notification-style haptic feedback.
//   - type: string - "SUCCESS" / "ERROR" / "WARNING"
//
// window.natively.hapticPattern(pattern, delay)
//   - Triggers a custom haptic vibration sequence.
//   - pattern: string - sequence of symbols:
//       O = heavy impact
//       o = medium impact
//       . = light impact
//       X = rigid impact
//       x = soft impact
//       - = wait 0.1 second
//   - delay: number - time between each element, must be >= 0 and < 1

// --- Haptic Feedback. Impact Example. Start ---

window.natively.hapticImpact("MEDIUM");

// --- Haptic Feedback. Impact Example. End ---


// --- Haptic Feedback. Notification Example. Start ---

window.natively.hapticNotification("SUCCESS");

// --- Haptic Feedback. Notification Example. End ---


// --- Haptic Feedback. Pattern Example. Start ---

const pattern = "..oO-Oo..";
const delay = 0.1; // time between each element, must be >= 0 and < 1
window.natively.hapticPattern(pattern, delay);

// --- Haptic Feedback. Pattern Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Haptic Feedback SDK: // window.natively.hapticImpact(type) — single haptic impact. type: "LIGHT" / "MEDIUM" / "HEAVY" / "RIGID" / "SOFT". // window.natively.hapticNotification(type) — notification haptic. type: "SUCCESS" / "ERROR" / "WARNING". // window.natively.hapticPattern(pattern, delay) — custom sequence. pattern symbols: O=heavy, o=medium, .=light, X=rigid, x=soft, -=wait 0.1s. delay: number >= 0 and &#x3C; 1 (time between each element). For reference: https://docs.buildnatively.com/guides/integration/haptic-feedback
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Trigger a success haptic feedback when the user completes a form submission".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Choose the right haptic type

Use **Impact** for simple physical responses to interactions like button taps, swipes, or toggles. Use **Notification** when you want to communicate a status - `SUCCESS` for completed actions, `ERROR` for failures, and `WARNING` for caution states. Use **Pattern** when you need a custom sequence, like a celebratory pulse or a repeating alert rhythm.

#### Match intensity to the interaction

Lighter impacts (`LIGHT`, `SOFT`) work well for subtle UI interactions like toggling a switch or selecting an item. Heavier impacts (`HEAVY`, `RIGID`) are better reserved for significant actions like confirming a payment or completing an important step.

#### Use haptics sparingly

Overusing haptic feedback makes it lose its meaning and can feel intrusive. Reserve it for meaningful moments - confirmations, errors, or interactions where physical feedback genuinely adds value.

#### Building patterns

The pattern string is read left to right, with each symbol triggering its corresponding impact and `-` adding a 0.1 second pause. For example `..oO-Oo..` creates a rising and falling sequence. Keep patterns short - long sequences can feel overwhelming.

#### Delay between pattern elements

The `delay` value controls the time between each element in the pattern. Must be `>= 0` and `< 1`. A value of `0` plays all elements as fast as possible, while `0.5` adds a noticeable pause between each step.

## Troubleshooting

<details>

<summary>Haptic feedback is not working</summary>

Haptic feedback requires a physical device - it will not work in simulators or emulators. Test on a real iOS or Android device.

</details>

<details>

<summary>Haptic feedback not working on a real device</summary>

Some devices or user settings may disable the haptic feedback system-wide. Check that the user hasn't disabled vibration or haptic feedback in their device settings.

</details>

<details>

<summary>Haptic Pattern not triggering</summary>

Verify the pattern string only contains valid symbols (`O`, `o`, `.`, `X`, `x`, `-`). Any unrecognized character may cause the pattern to fail silently.

</details>

<details>

<summary>Haptic Pattern delay not working as expected</summary>

The delay value must be `>= 0` and `< 1`. A value of `1` or higher will not work. If your pattern feels too fast or too slow, adjust the delay value within the valid range.

</details>

<details>

<summary>No haptic feedback on Android</summary>

Some older or budget Android devices may have limited or no haptic motor support. This is a hardware limitation and cannot be resolved in the app.

</details>

[^1]: Replace this placeholder


# HealthKit

* [General Info](#general-info)
* [Bubble.io Web Apps](#bubble.io-web-apps)
* [Other Websites](#other-websites)

## General Info

HealthKit is a framework developed by Apple that allows health and wellness data to be collected, analyzed, and shared across different applications.

By taking advantage of the HealthKit framework, you can create a more robust and effective health and wellness app that meets the needs of today’s mobile-savvy users.

HealthKit integration can improve the user experience of your health and wellness app, provide more accurate data and insights, increase user engagement, and enhance data security and privacy. By taking advantage of the HealthKit framework, you can create a more robust and effective health and wellness app that meets the needs of today’s mobile-savvy users.

HealthKit is a huge framework, but Natively has access to several values (for now). Quantity, characteristics, and category.

1. Each of these types **needs** to be **requested permission** to read them.
2. **Quantity Values** are calculated with a default Apple's [HKStatisticsCollection](https://developer.apple.com/documentation/healthkit/hkstatisticscollectionquery)
3. All **Quantity Values** have different values to work with (e.g. milliseconds, count/s, count/min, etc.)

**Quantity Values:**

* &#x20;**HRV (Heart Rate Variability SDNN)**

  The standard deviation of heartbeat intervals, measured in ***milliseconds***
* **RHR (Resting Heart Rate)**

  User’s resting heart rate, measured in ***count/s***
* **BMI (Body Mass Index)**\
  User’s body mass index, measured in ***count***
* **HEIGHT**\
  User’s height, measured in ***centimeters***
* **BODY\_MASS - User's body mass**\
  User's body mass, measured in ***kilograms***
* **STEPS**\
  User's steps, measured in ***count***
* **HEART\_RATE**\
  User's heart rate, measured in ***count/min***
* **ACTIVE\_ENERGY**\
  User's burned active energy, measured in ***kilocalories***
* **BLOOD\_OXYGEN**\
  User's blood oxygen, measured in ***percent***

**Characteristics Values:**

* **DATE\_OF\_BIRTH**\
  User's age, measured in ***years*** *(e.g. **67**)*
* **BLOOD\_TYPE**\
  User's blood type (e.g. ***AB- / AB+ / A- / B+ / B- / B+ / O- / O+ / NOT\_SET**)*
* **SEX**\
  User's biological sex - (e.g. ***MEN / WOMAN / OTHER / NOT\_SET**)*
* **SKIN\_TYPE**\
  User's skin type - (e.g. ***I / II / III / IV / V / VI / NOT\_SET**)*
* **WHEELCHAIR**\
  A value indicating the user’s wheelchair use - (e.g. ***YES / NO / NOT\_SET**)*

**Category Values:**

* [**SLEEP\_ANALYSIS**](https://developer.apple.com/documentation/healthkit/hkcategoryvaluesleepanalysis)

  Has a few different types:

  * **IN\_BED**
  * **AWAKE**
  * **ASLEEP** - available only in iOS 16+ (only with Apple Watch)
  * **ASLEEP\_REM** - available only in iOS 16+ (only with Apple Watch)
  * **ASLEEP\_DEEP** - available only in iOS 16+ (only with Apple Watch)
  * **ASLEEP\_UNSPECIFIED**
  * **UNKNOWN**

<figure><img src="https://docs-assets.developer.apple.com/published/88d1eb5c0f/renderedDark2x-1667248100.png" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
More details about each Sleep Analysis value can be found [here](https://developer.apple.com/documentation/healthkit/hkcategoryvaluesleepanalysis)
{% endhint %}

* **ACTIVITY\_SUMMARY**\
  Main fields:
  * **Active Energy Burned** (.activeBurned) - kilocalories
  * **Active Energy Goal** (.activeGoal) - kilocalories
  * **Exercise Time** (.exerciseTime) - time interval in minutes
  * **Exercise Goal** (.exerciseGoal) - time interval in minutes
  * **Move Time** (.moveTime) - time interval in minutes **(available only in iOS 14+)**
  * **Move Goal** (.moveGoal) - time interval in minutes **(available only in iOS 14+)**
  * **Stand Hours** (.standHours) - count
  * **Stand Goal** (.standGoal) - count

{% hint style="info" %}
More details about each Activity Summary value can be found [here](https://developer.apple.com/documentation/healthkit/hkactivitysummary)
{% endhint %}

* **WORKOUTS (JS SDK >= 2.13.0)**\
  Main fields:
  * **Total Energy Burned - kilocalories**
  * **Basal Energy Burned - kilocalories (>iOS 16)**
  * **Active Energy Burned - kilocalories (>iOS 16)**
  * **Workout Name - string, more details** [**here**](https://stackoverflow.com/a/71454827)
  * **Heart Rate - count/min**
  * **Start Date - date**
  * **End Date - date**
  * **Duration - seconds**<br>

**To implement HealthKit correctly you need to:**

1. [Turn on **DEBUG** mode](/guides/integration/how-to-get-started#mode-headers-optional) for a development purpose
2. Check if HealthKit is available on a device. (iPads are not supported)
3. Request permissions for specific Values (e.g. SKIN\_TYPE, HEART\_RATE, SLEEP\_ANALYSIS, WORKOUTS)
4. We're highly recommending writing a good [Permission Description](/natively-platform/features/healthkit) that explains to the user why you need such permission (otherwise, you will be rejected by Apple, or the User will not give access to specific data)
5. Request the data and receive it
6. All values requests are taking some time (except characteristics values). The calculation happens on a device, so if you're requesting a lot of data for an extended period (3-12 months), it might take 5-30 seconds to return it.

## Bubble.io Web Apps

HealthKit can be integrated with our Bubble.io plugin. More details about it can be found [here](/guides/integration/how-to-get-started).

Plugin contains 3 elements: **Natively - HealthKit BASE,** **Natively - HealthKit SLEEP** & **Natively - HealthKit ACTIVITY**

The first is used for general purposes like requesting permissions and getting characteristics and quantity values. The second is specifically for Category Value **Sleep Analysis.** And the third is for the **Activity Summary.**

### 1. Natively - HealthKit BASE

#### Actions:

* Get Health Availability - Checking if HealthKit is available on a device
* Request Permissions
  * read (e.g. **HRV, RHR, SLEEP\_ANALYSIS, BMI, etc.)**
* Get Permission Status - Checking if you've requested permission before (**After requesting permission once, it might return YES every time, it doesn't guarantee that the user has provided access to their data cause of privacy reasons by Apple**)
  * type (e.g. **HRV, RHR, SLEEP\_ANALYSIS, BMI, etc.)**
* Get All **Characteristic Values**
* Get Statistic **Quantity Values**
  * data\_type (**Quantity Values** only!) (e.g. **HRV, RHR, BMI, etc.)**
  * interval (SECOND / MINUTE / HOUR / DAY / WEEK / MONTH / YEAR) - means how HealthKit will return a data \
    Daily, Weekly ... and so on.
  * start\_date - The start date from which HealthKit will search and return a data
  * end\_date - End date till which HealthKit will search and return a data

{% hint style="info" %}
So basically, if you wanna get the daily steps to count for the current month you will need to select **DAY** interval with **start\_date=startofmonth** & **end\_date=today**
{% endhint %}

#### Events:

* Get Permission Value Updated - Get called once the state updated
* Get Health Availability Value Updated - Get called once the state updated
* Request Permission Value Updated - Get called once the state updated
* Get All Characteristics Updated - Get called once the state updated
* Get Quantity Updated - Get called once the state updated

#### States:

* Latest Get Health Availability Result
* Latest Get Permission Result - **After requesting permission once, it might return YES every time. It doesn't guarantee that the user has provided access to their data cause of privacy reasons by Apple**
* Latest Request Permission Result
* Latest All Characteristics (CharacteristicObject)
  * Age - number
  * Sex - text  (e.g. ***MEN / WOMAN / OTHER / NOT\_SET**)*
  * Blood - text  (e.g. ***AB- / AB+ / A- / B+ / B- / B+ / O- / O+ / NOT\_SET**)*
  * Skin - text  (e.g. ***I / II / III / IV / V / VI / NOT\_SET**)*
  * Wheelchair - text  (e.g. ***YES / NO / NOT\_SET**)*
* Latest Get Quantity Response - (QuantityStatisticsObject)

{% hint style="warning" %}
IMPORTANT! Set QuantityStatisticsObject & CharacteristicObject type from a list in the element field.\
No need to create a new type for this. It's already defined.
{% endhint %}

Here is a small example of setup:

{% embed url="<https://bubble.io/page?id=nativelyqa&name=healthkit_base&tab=tabs-1&test_plugin=1654595882459x381599056563798000_current&type=page>" %}

### 2. Natively - HealthKit SLEEP

SLEEP element is used in combination with BASE. So basically, you need the BASE element to request permissions and SLEEP for requesting sleep analysis data.

#### Actions:

* Get Daily Sleep Analysis
  * limit (e.g. 100) - related to a count of data that will be fetched from HealthKit and calculated. You can use any value that works best for you. To remove a limit, set a value to 0.
  * end\_date - End date till which HealthKit will search and return a data
  * start\_date - The start date from which HealthKit will search and return a data

#### Events:

* Get Daily Sleep Analysis Updated

#### States:

* Latest Get Sleep Analysis Response - (SleepAnalysisObject)

{% hint style="warning" %}
IMPORTANT! Set SleepAnalysisObject type from a list in the element field.\
No need to create a new type for this. It's already defined.
{% endhint %}

Here is a small example of setup:

{% embed url="<https://bubble.io/page?id=nativelyqa&name=healthkit_sleep&tab=tabs-1&test_plugin=1654595882459x381599056563798000_current&type=page>" %}

### 3. Natively - HealthKit ACTIVITY

ACTIVITY element is used in combination with BASE. So basically, you need the BASE element to request permissions and ACTIVITY for requesting sleep analysis data.

#### Actions:

* Get Activity Summary
  * end\_date - End date till which HealthKit will search and return a data
  * start\_date - The start date from which HealthKit will search and return a data

#### Events:

* Get Activity Summary Updated - Get called once the state updated

#### States:

* Latest Get Activity Summary Response - (ActivitySummaryObject)

{% hint style="warning" %}
IMPORTANT! Set ActivitySummaryObject type from a list in the element field.\
No need to create a new type for this. It's already defined.
{% endhint %}

Here is a small example of setup:

{% embed url="<https://bubble.io/page?id=nativelyqa&name=healthkit_activity&tab=tabs-1&test_plugin=1654595882459x381599056563798000_current&type=page>" %}

### 4. Natively - HealthKit WORKOUT

WORKOUT element is used in combination with BASE. So basically, you need the BASE element to request permissions and WORKOUT for requesting workouts data.

#### Actions:

* Get Workouts
  * limit (e.g. 100) - related to a count of data that will be fetched from HealthKit and calculated. You can use any value that works best for you. To remove a limit, set a value to 0.
  * end\_date - End date till which HealthKit will search and return a data
  * start\_date - The start date from which HealthKit will search and return a data

#### Events:

* Get Workout Updated

#### States:

* Latest Get Workout Response - (WorkoutObject)

{% hint style="warning" %}
IMPORTANT! Set WorkoutObject type from a list in the element field.\
No need to create a new type for this. It's already defined.
{% endhint %}

Here is a small example of setup:

{% embed url="<https://bubble.io/page?id=nativelyqa&name=healthkit_workout&tab=tabs-1&test_plugin=1654595882459x381599056563798000_current&type=page>" %}

## Other websites

#### Refer for types, and values on Bubble Doc ^

For methods, name-check the NativelyHealth object interface by following this [URL](https://github.com/No-Code-No-Problem/natively-sdk/blob/2680e4f032b8cfde99090d5c557c00e39cc2bbbf/natively-frontend.js#L545-L590)

### 'Get Workouts' Example (SDK >=2.13.0)

```javascript
// 4. Natively HealthKit Workout
const health = NativelyHealth()
// 1. Check HealthKit availability
health.available((res, err) => {
    if (err) {
        alert(err)
        return;
    }
    if (res.status) {
        // 2. Request permission
        health.requestAuthorization([], ["WORKOUTS"], (res, err) => {
            if (err) {
                alert(err)
                return;
            }
            if (res.status) { // true/false
                const today = new Date();
                const weekAgo = new Date();
                weekAgo.setDate(today.getDate() - 7);
                
                // 3. Get workouts in date range
                health.getWorkouts(today, weekAgo, (res, err) = {
                    if (err) {
                        alert(err)
                        return;
                    }
                    const result = res.result;
                    console.log(result);
                    /*
                    {
                      "endDate": 1676945111,
                      "duration": 3000, // seconds
                      "startDate": 1676945291,
                      "workoutName": "workoutName", // https://stackoverflow.com/a/71454827
                      "activeBurned": 0, // kilocal >=iOS 16
                      "basalBurned": 0, // kilocal >=iOS 16
                      "totalBurned": 0, // kilocal
                      "heartRate": 0 // count/min >=iOS 16
                    }
                    */
                })
            }
        })
    }
})
```


# Insets (Safe Area)

getInsets method allows developers to accurately identify and adjust app layouts based on device screen insets, such as those around notches or camera cutouts.

```javascript
window.natively.getInsets((resp) => {
        console.log("Resp:", JSON.stringify(resp, null, 2)); 
      });
```


# Loading Screen

\>= v2.12.0

* [Bubble.io Plugin](#bubble.io-plugin)
* [JavaScript SDK](#javascript-sdk)

### 🧋 Bubble.io Plugin

#### \[Action] Natively - Show Loading Screen

* Auto Hide - Automatically hides screen after page loaded. \
  !!! Use this carefully since the user can stack on the Loading screen.

#### \[Action] Natively - Hide Loading Screen

### 🛠 JavaScript SDK

#### Show Loading Screen

{% code overflow="wrap" lineNumbers="true" %}

```javascript
// Automatically hides screen after page loaded. 
// !!! Use this carefully since the user can stack on the Loading screen.
const autoHide = true; 
window.natively.showLoadingScreen(autoHide);
```

{% endcode %}

#### Hide Loading Screen

```javascript
window.natively.hideLoadingScreen();
```


# Localization

To provide a truly native experience, Natively supports localized Permission Descriptions and App Names. When a user's device is set to one of the following languages, the system will automatically display the corresponding translation you have provided in the dashboard.

### Supported Languages

Natively currently supports AI-powered and manual translation for the following locales:

* English (Default Base)
* Arabic
* Chinese (Simplified)
* Dutch
* French
* German
* Italian
* Korean
* Portuguese
* Spanish

{% hint style="warning" %}
If a user's device is set to a language not included in your active translations, the app will fall back to the app's primary language.
{% endhint %}

### Managing Feature Translations

Localization is handled within the specific configuration of each feature to ensure context is maintained.

#### The workflow

1. **Accessing Translations:** Navigate to the Features tab, select a feature (e.g., Notifications or Camera), and click Manage under the Permission description section.\ <br>

   <figure><img src="/files/YkkVw1vERyZIKrpgHPoV" alt="" width="375"><figcaption></figcaption></figure>

2. **The "Base Language":** Your primary language acts as the "Source of Truth." Finalize your primary text first before proceeding to other languages.\ <br>

   <figure><img src="/files/du426yL4zgxqC4WZ1tgP" alt="" width="353"><figcaption></figcaption></figure>

3. **Individual Translation:** Select a target language tab (e.g., Italian). You can manually type your text or use the AI auto-translate button for an instant draft.\ <br>

   <figure><img src="/files/ho7Fs1JNQNFXvOd5NDev" alt="" width="354"><figcaption></figcaption></figure>

### Global Bulk Translations

To save time when managing several features, use bulk synchronization actions.

**Apply to all**: If you update your primary base text, use the Apply to All button in the translation drawer to sync that change across all other active languages instantly.

<figure><img src="/files/G5OjuROeUmZdY7uUQKru" alt="" width="355"><figcaption></figcaption></figure>

{% hint style="warning" %}
This action will overwrite all existing manual translations for that feature. Use this only when you have made significant changes to your core messaging.
{% endhint %}

### Verification & Best Practices

Following these rules will protect your app from being rejected during the App Store or Google Play review process.

* **Character Limits:** Mobile systems have strict UI limits. If your permission description is too long, the counter will turn red.
* **Manual Review:** Always verify AI-generated text. Automated translations can sometimes miss technical context or use a tone that doesn't fit your brand.
* **Truncation Warning:** Be aware that Romance languages (such as Spanish, French, Italian, and Portuguese) are typically 30–35% longer than English. If an automated translation exceeds the limit, the system will automatically truncate (cut off) the text. This often happens in the middle of a sentence, causing the description to lose its meaning and potentially leading to App Store rejection.
* **Sync Status:** Monitor your translation manager for these indicators:
  * ✓ Saved and ready for build.
  * ⚠️ Your translation is outdated.

### Changing the Primary App Language

Changing your Primary Language in the App Settings is a global action that affects your entire project.

1. **Global Update:** Changing the base language triggers an automatic AI re-translation of every Permission Description in your app to match the new source.
2. **Manual Audit:** After a base language change, you must manually verify all features before build.

<figure><img src="/files/jWEx4948jUkZvPstcvPo" alt="" width="549"><figcaption></figcaption></figure>

{% hint style="info" %}
**Note on App Store Listings**\
Please note that the primary language you select here will also dictate the default language visible for your application in the Apple App Store and Google Play Store.
{% endhint %}

### App Name Localization

The App Name is localized separately within the App Settings. This ensures that the primary identity of your app remains consistent or is specifically tailored to each market.

Unlike Permission Descriptions, the App Name is NOT automatically translated when you add a new language through the Feature Translation drawer.

Before triggering a new build, please navigate to App Settings to verify that your App Name has been correctly translated for all active languages.

### Permission Description Templates

Use these templates as a baseline to ensure your app meets Apple and Google’s strict transparency requirements.

{% hint style="warning" %}
**Disclaimer:** These are examples only and are not "ready-to-use" final texts. Every app has individual and unique reasons for requesting hardware access. You must always be transparent and specific with your users about *why* your particular app needs a permission. Failing to provide an honest, app-specific reason is the #1 cause for App Store rejections.
{% endhint %}

| Camera              | "Use your camera to take profile photos, scan QR codes, and upload images directly to the app."                        |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| Photo Library       | "Select and upload existing photos from your library to customize your profile and share content."                     |
| Microphone          | "Access to the microphone is required to record voice messages and capture audio for video uploads."                   |
| NFC                 | "This allows the app to scan physical NFC tags and interact with supported hardware devices."                          |
| Location            | "Your location is used to provide relevant local content, map features, and personalized recommendations near you."    |
| Location Background | "Enable background location to receive important proximity alerts and automated features even when the app is closed." |
| Notifications       | "Stay updated with real-time alerts, reminders, and important account activity directly on your screen."               |
| Contacts            | "Sync your contacts to easily find and connect with friends or colleagues already using the app."                      |
| Calendars           | "Access to your calendar allows you to save important events, book appointments, and stay organized."                  |
| HealthKit (Read)    | "We read your health data to track your fitness progress and provide personalized wellness insights."                  |
| HealthKit (Write)   | "This allows the app to save your activity and health updates directly to your Apple Health dashboard."                |
| Apple ATT           | "Your data will be used to provide a more personalized experience and deliver content that matches your interests."    |
| Admob               | "This allows us to show you relevant advertisements and helps keep the app free for all users."                        |
| Analytics           | "We collect anonymous data to understand app performance and improve your user experience with every update."          |

### Implementation <a href="#implementation" id="implementation"></a>

Choose your integration method below: **Bubble.io Plugin** (No-Code) or **JavaScript SDK** (Code).

{% tabs %}
{% tab title="Bubble.io" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

If it is NOT installed: Click the + Add plugins button , search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="Javascript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.25.2`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.25.2/natively-frontend.min.js"></script>
</head>
```

{% endtab %}
{% endtabs %}

#### Setup logic

{% tabs %}
{% tab title="Bubble.io" %}
Drag the Natively - Audio Player element onto your page.

{% hint style="warning" %}
This element must be set to Visible on page load to initialize correctly. It should be placed directly on the page root and not inside hidden containers, such as Popups, Floating Groups, Group Focus elements, or Repeating Groups. To hide the element from your UI, you may set its dimensions to 0x0 px.
{% endhint %}

<figure><img src="/files/4SCmXBHtituv3vy0KEeO" alt=""><figcaption></figcaption></figure>

**Element Logic (Events, States, & Actions)**

Events:

* Locales Received: Fires immediately after a successful `Get Locales` action. Use this event to populate a custom state or a dropdown menu with the newly fetched `Locales` list.
* Locale Updated: Fires immediately after a successful `Set Locale` action. Use this event to refresh the page data or show a "Language updated successfully" alert.
* Error Occurred: Fires if any action fails. Use this to trigger an error popup displaying the element's `Error message` state.

States:

* `Status` (Text): Returns the current operational status (e.g., `SUCCESS`).
* `Error message` (Text): Returns the description of the error encountered during a failure.
* `Locales` (List of texts): An array of all locale codes supported by the app (e.g., `["en", "fr", "es"]`).
* `Current` (Text): The currently active locale code being used by the app (e.g., `"fr"`).
* `Default` (Text): The app's original default or fallback locale.

Actions:

* Get Locales: Retrieves the app's current and default locale details and a list of all supported languages.
* Set Locale: Updates the app's active language to your specified locale code.\
  &#x20;     `Locale` (Text): The target language code (e.g., `fr`).
  {% endtab %}

{% tab title="Javascript SDK" %}

```javascript
// ============================================================================
// NATIVELY LOCALIZATION - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const nativelyLocale = new NativelyLocale();

// ============================================================================
// ALL AVAILABLE METHODS & PARAMETERS
// ============================================================================

// nativelyLocale.getLocales(callback); 
//   - Retrieves the device's supported locales and the current active language.
//   - Callback Response Object includes:
//       * status (String): "SUCCESS" or "ERROR".
//       * locales (Array of Strings): All locale codes supported by the device (e.g., ["en", "fr"]).
//       * current (String): The currently active locale code.
//       * default (String): The device's original default/fallback locale.
//       * message (String): Error details (if status is "ERROR").
//
// nativelyLocale.setLocale(targetLocale, callback); 
//   - Updates the application's active language to your specified locale code.
//   - targetLocale (String): The target language code (e.g., "es").
//   - Callback Response Object includes:
//       * status (String): "SUCCESS" or "ERROR".
//       * message (String): Error details (if status is "ERROR").


// --- Localization. Quick Start & Flow. Start ---

// 1. DEFINE CALLBACKS
const getLocalesHandler = function(resp) {
    if (resp && resp.status === "SUCCESS") {
        console.log("Current Active Language:", resp.current);
        console.log("Device Default Language:", resp.default);
        console.log("All Supported Languages:", resp.locales); // e.g., ["en", "fr", "es"]
        
        // Example: You could populate a dropdown menu in your UI using resp.locales here.
    } else {
        const errorMsg = resp ? resp.message : "Unknown error occurred.";
        console.error("Failed to fetch locales:", errorMsg);
    }
};

const setLocaleHandler = function(resp) {
    if (resp && resp.status === "SUCCESS") {
        console.log("Language updated successfully!");
        // Example: Trigger a page reload or update your UI text automatically.
    } else {
        const errorMsg = resp ? resp.message : "Failed to set locale.";
        console.error("Error updating language:", errorMsg);
    }
};

// 2. CORE FLOW
// Fetch the available languages as soon as the app loads
nativelyLocale.getLocales(getLocalesHandler);

// Later, when a user clicks a "Change Language" button in your UI
const userSelectedLanguage = "fr";
// nativelyLocale.setLocale(userSelectedLanguage, setLocaleHandler);

// --- Localization. Quick Start & Flow. End ---
```

{% endtab %}
{% endtabs %}

### Live Demo & Editor Example

{% tabs %}
{% tab title="Bubble.io" %}
To see a working implementation of the Localization feature we highly recommend exploring our demo application. You can test the live functionality or open the Bubble Editor to inspect the exact workflow configurations and reverse-engineer the setup for your own app.

* [View Live Demo](https://nativelyqa.bubbleapps.io/version-test/localization)
* [Inspect in Bubble Editor](https://bubble.io/page?id=nativelyqa\&test_plugin=1654595882459x381599056563798000_current\&tab=Design\&name=localization\&type=page\&elements=cnGPW0)
  {% endtab %}
  {% endtabs %}

### Troubleshooting

If the feature isn't behaving as expected, the [Debug Console](/guides/integration/debug-console) is your best friend. It reveals the conversation between your web app and the native app.

If you cannot resolve the issue using the logs, our team is here to help. To solve your issue on the first reply, we require a Standardized Bug Report based on your debug data.

Your report must include:

1. App ID: Provide the unique ID found in Natively Dashboard > Settings.
2. Actual Behavior: A clear description of what is happening (or not happening).
3. Expected Behavior: A clear description of what the app should be doing.
4. Steps to Reproduce: A list of the exact actions needed to trigger the error.
5. Console Screenshot: A capture of the Debug Console showing the specific error logs.
6. Logic Configuration: Screenshots of the specific logic where the error occurs (e.g., Bubble workflows, API connectors, or code snippets).
7. Test Credentials: If the issue requires a login to reproduce, provide a set of working test credentials (User/Pass).


# Open External URL

Open web URLs inside your app or in the device's default browser.

## What is Open External URL?

Open External URL lets you control how web links open in your app. You can display them inside a temporary in-app browser window - keeping users within your app - or hand them off to the device's default browser when the external context is needed.

* **In-App Browser** - opens the URL in a temporary browser window inside your app. The user can close it and return to your app with a single tap. Best for web pages, articles, or Terms of Service.
* **System Browser** - opens the URL in the device's default browser (Safari on iOS, Chrome on Android). Best for file downloads or external services that require the user's saved passwords or browser history.

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Action] Natively - Open external url

* **URL** - the web URL to open.
* **Open in external browser** - Yes/No:
  * **Yes** - opens in the system browser.
  * **No** - opens in the in-app browser.

<figure><img src="/files/J8fip6Zgjpt731GxE65K" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}

```javascript

// ============================================================================
// NATIVELY OPEN EXTERNAL URL - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — openExternalURL is available directly
// on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.openExternalURL(url, external)
//   - Opens a web URL either in the in-app browser or the system browser.
//   - url: string — the web URL to open. Must start with https://
//   - external: boolean — true = system browser, false = in-app browser

// --- Open External URL. In-App Browser Example. Start ---

window.natively.openExternalURL("https://example.com/", false);

// --- Open External URL. In-App Browser Example. End ---


// --- Open External URL. System Browser Example. Start ---

window.natively.openExternalURL("https://example.com/", true);

// --- Open External URL. System Browser Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Open External URL SDK: // window.natively.openExternalURL(url, external) — opens a web URL. url: string — must start with https://. external: boolean — true opens in system browser, false opens in in-app browser. For reference: https://docs.buildnatively.com/guides/integration/open-external-url
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Open the Terms of Service page in the in-app browser when the user taps the link".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Internal URLs - keep specific domains in your app's main view

If you want a specific domain to always open directly inside your app's main WebView - without any browser UI or close button - add it to your [**Internal URLs**](/natively-platform/settings#internal-urls) whitelist in the Natively Dashboard [Settings](/natively-platform/settings). This is useful when your app spans multiple subdomains or when you use a third-party service that must stay within your app context.

#### Default behavior for unwhitelisted domains

Any link your app navigates to on a domain not in your [Internal URLs](/natively-platform/settings#internal-urls) whitelist will automatically open in the in-app browser - even without calling `openExternalURL`. Use `openExternalURL` with `external: true` when you specifically need the system browser for one of those links.

#### Use the in-app browser for content

Open articles, Terms of Service, Help Center pages, or any web content the user should read and then return from. The in-app browser shows a close button, so users can always get back to your app with a single tap.

#### Use the system browser for downloads and external services

File downloads and external services that rely on saved passwords or browser history should always use `external: true`. These require the full system browser context to work correctly.

## Troubleshooting

<details>

<summary>URL opens in the wrong browser</summary>

Check the `external` parameter - `true` forces the system browser, `false` forces the in-app browser. Both values override the [Internal URLs](/natively-platform/settings#internal-urls) whitelist. If you're not using `openExternalURL` and the URL is opening unexpectedly, check your Internal URLs whitelist in the Natively Dashboard [Settings](/natively-platform/settings).

</details>

<details>

<summary>URL opens inside the app instead of the in-app browser</summary>

If you're not calling `openExternalURL`, the domain may be whitelisted in [Internal URLs](/natively-platform/settings#internal-urls), causing it to open in the app's primary WebView. Either remove it from the whitelist, or explicitly call `openExternalURL(url, false)` to force the in-app browser.

</details>

<details>

<summary>In-app browser not showing close button or navigation bar</summary>

The domain is likely whitelisted in [Internal URLs](/natively-platform/settings#internal-urls) and opens in the app's primary WebView instead. Either remove it from the whitelist or explicitly call `openExternalURL(url, false)` to force the in-app browser.

</details>

<details>

<summary>Not working in a web browser</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build or the Preview app.

</details>

[^1]: Replace this placeholder


# Open External App

Launch other installed apps directly from your app using URL schemes.

## What is Open External App?

Open External App lets your app launch other installed applications on the user's device using URL schemes. This is useful for directing users to specific content in another app - opening a location in Google Maps, starting a WhatsApp chat, triggering the phone dialer with a pre-filled number, or any other app that supports deep linking via a URL scheme.

{% hint style="info" %}
If the target app is not installed on the user's device, the command will be silently ignored by the OS - nothing will happen.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature requires any **paid** plan. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Common App Schemes

A URL scheme is a string that identifies an app on the device - similar to how `https://` identifies a web URL.

The table below lists URL schemes for popular apps, along with the correct value to enter in your Natively Dashboard and the scheme to use when calling `openExternalApp`.

{% hint style="danger" %}
App schemes can change between app versions, and Natively does not guarantee their accuracy. If a scheme stops working, check the app's official documentation. For a comprehensive list beyond the table below, refer to community-maintained references such as [github.com/bhagyas/app-urls](https://github.com/bhagyas/app-urls).
{% endhint %}

{% hint style="info" %}
On Android, you can open any app using its Bundle ID followed by `://` - for example `com.whatsapp://` - even if you don't know the app's custom URL scheme.&#x20;

This does not work on iOS, which requires the app's registered custom URL scheme.
{% endhint %}

{% hint style="warning" %}
Replace any value in `[brackets]` with your actual data - remove the brackets themselves. For example, `tel:[PHONE_NUMBER]` becomes `tel:+1234567890`.
{% endhint %}

<table data-search="false"><thead><tr><th width="133.77783203125" align="center">App</th><th width="130.22222900390625" align="center">External App Scheme</th><th width="480.4447021484375" align="center">Usage Example</th></tr></thead><tbody><tr><td align="center">Phone Dialer</td><td align="center"><code>tel</code></td><td align="center"><code>tel:[PHONE_NUMBER]</code></td></tr><tr><td align="center">Email</td><td align="center"><code>mailto</code></td><td align="center"><code>mailto:[EMAIL_ADDRESS]</code></td></tr><tr><td align="center">WhatsApp</td><td align="center"><code>whatsapp</code></td><td align="center"><code>whatsapp://send?phone=[PHONE_NUMBER]</code></td></tr><tr><td align="center">Telegram</td><td align="center"><code>tg</code></td><td align="center"><p><code>tg://</code></p><p><code>tg://msg?text=[MESSAGE]</code></p><p><code>tg://resolve?domain=[TELEGRAM_USERNAME]</code></p></td></tr><tr><td align="center">Facebook</td><td align="center"><code>fb</code></td><td align="center"><code>fb://</code><br><code>fb://profile?id=[FACEBOOK_ID]</code><br><code>fb://page?id=[PAGE_ID]</code></td></tr><tr><td align="center">Twitter / X</td><td align="center"><code>twitter</code></td><td align="center"><p><code>twitter://user?screen_name=[USERNAME]</code></p><p><code>twitter://post?message=[MESSAGE]</code></p></td></tr><tr><td align="center">Waze</td><td align="center"><code>waze</code></td><td align="center"><p><code>waze://</code></p><p><code>waze://?q=[LOCATION]</code></p></td></tr><tr><td align="center">App Store (iOS only)</td><td align="center"><code>itms-apps</code></td><td align="center"><code>itms-apps://itunes.apple.com/app/id[APP_STORE_APP_ID]</code></td></tr><tr><td align="center">Play Store (Android only)</td><td align="center"><code>market</code></td><td align="center"><code>market://details?id=[PLAY_STORE_APP_BUNDLE_ID]</code></td></tr></tbody></table>

## Natively Dashboard Setup

Before using `openExternalApp`, you must whitelist the URL scheme of the target app in your Natively Dashboard.

1. Open your Natively app dashboard and navigate to **Settings** > **External App Schemes**.
2. Enter the [URL scheme](#common-app-schemes) of the app you want to open.
3. Click **Add**.
4. Repeat for each app you want to support.
5. Click **Save**.
6. Rebuild your app(s).

{% hint style="warning" %}
You must rebuild your app for these changes to take effect. A new rebuild is required each time you add or modify **External App Schemes**.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Action] Natively - Open external app

* **App URL** — the app's URL scheme including `://` (e.g. `whatsapp://send?phone=+1234567890`).

<figure><img src="/files/56m2A6hCH308NEzzQqda" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY OPEN EXTERNAL APP - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — openExternalApp is available directly
// on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.openExternalApp(scheme)
//   - Opens another installed app using its URL scheme.
//   - scheme: string — the app's URL scheme including ://
//              e.g. "whatsapp://", "whatsapp://send?phone=+1234567890"

// --- Open External App. Example. Start ---

window.natively.openExternalApp("whatsapp://send?phone=+1234567890&text=Hello");

// --- Open External App. Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>], schemes to use [<a data-footnote-ref href="#user-content-fn-2">URL schemes to use</a>], using the Natively Open External App SDK: // window.natively.openExternalApp(scheme) — opens another installed app using its URL scheme. scheme: string — the app's URL scheme including ://, e.g. "whatsapp://send?phone=+1234567890". The scheme must be whitelisted in Natively Dashboard → Settings → External App Schemes before it will work. For reference: https://docs.buildnatively.com/guides/integration/open-external-app
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Open WhatsApp with a pre-filled message when the user taps the contact support button".
{% endhint %}

{% hint style="warning" %}
Replace the placeholder **URL schemes to use** with those configured in your Natively Dashboard and the required path within the scheme.
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Open a specific chat or contact

The most common use case - opening WhatsApp, Telegram, or Facebook with a pre-filled phone number or username so the user can start a conversation in one tap, without having to find the contact themselves.

#### Open the phone dialer

Use `tel:[PHONE_NUMBER]` to pre-fill the dialer with a support number or contact. The user still has to tap call button - nothing is dialed automatically.

#### Handle the app not being installed

If the target app is not installed, nothing happens - the OS silently ignores the call. Consider showing a message to the user first, or falling back to a web URL using [Open External URL](/guides/integration/open-external-url).

## Troubleshooting

<details>

<summary>Nothing happens when calling <code>openExternalApp</code></summary>

Make sure the URL scheme is added to **Settings** > **External App Schemes** in your Natively Dashboard and that you've rebuilt your app after adding it. Without both steps, `openExternalApp` will not work.

</details>

<details>

<summary>The wrong app opens</summary>

Multiple apps can register the same URL scheme. If an unexpected app opens, the device has a different app set as the default handler for that scheme. This is an OS-level behavior and cannot be controlled from Natively.

</details>

<details>

<summary>The scheme works on iOS but not on Android (or vice versa)</summary>

Some apps use different schemes on iOS and Android. Check the app's official documentation for the correct scheme per platform. On Android, you can also try using the app's Bundle ID followed by `://` as an alternative.

</details>

<details>

<summary>App opens but navigates to the wrong place</summary>

The scheme parameters may be incorrect, or the app may have updated its URL scheme format. Check the app's latest documentation for the correct parameters.

</details>

[^1]: Replace this placeholder

[^2]: Replace this with your URL schemes


# Open App Settings

Direct users to your app's system settings page on their device.

## What is Open App Settings?

Open App Settings sends the user directly to your app's system settings page - the screen where they can manage permissions like Camera, Location, Microphone, and Notifications for your specific app. This is useful when a user has denied a permission, and you want to give them an easy way to re-enable it without having to navigate through their device settings manually.

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Action] Natively - Open App Settings

Opens the device's system settings page for your app. No parameters required.
{% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY OPEN APP SETTINGS - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — openAppSettings is available directly
// on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.openAppSettings()
//   - Opens the device's system settings page for your app.
//   - No parameters required. No callback.

// --- Open App Settings. Example. Start ---

window.natively.openAppSettings();

// --- Open App Settings. Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Open App Settings SDK: // window.natively.openAppSettings() — opens the device's system settings page for your app. No parameters or callback required. For reference: https://docs.buildnatively.com/guides/integration/open-app-settings
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Add a button that opens the app settings when the user has denied camera permission".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Redirect after a denied permission

The most common use case is showing a prompt when a user has denied a permission - Camera, Location, Microphone, Notifications - and offering them a button to go directly to settings to re-enable it. This is much better UX than asking the user to find the settings themselves.

#### Don't open settings without context

Always explain to the user why you're sending them to settings before triggering `openAppSettings()`. A clear message like "Camera access is required to scan QR codes. Please enable it in your app settings." prevents confusion.

#### Pair with permission status checks

Use feature-specific permission checks to detect when a permission is denied before showing the settings prompt. Only surface the button when it's actually needed.

## Troubleshooting

<details>

<summary>Nothing happens when <code>openAppSettings()</code> is called</summary>

Make sure the feature is triggered from a user interaction - like a button tap - rather than automatically on page load. Also verify the SDK is fully loaded before calling the method.

</details>

<details>

<summary>Not working in a web browser</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build or the Preview app.

</details>

<details>

<summary>Opens the wrong settings screen</summary>

`openAppSettings()` opens the system settings page specifically for your app - not a general settings menu. If the user is being taken to an unexpected place, verify you are using the correct method and not `openExternalURL` pointing to a settings URL.

</details>

[^1]: Replace this placeholder


# PDF Viewer

Display PDF files natively inside your app from a URL or Base64-encoded content.

## What is a PDF Viewer?

The PDF Viewer lets your app open and display PDF files using a native viewer - no need to redirect users to an external browser or ask them to download the file first. You can load a PDF from a public URL or pass Base64-encoded content directly. A single toggle controls both the download and share buttons, letting users save the file to their device or share it with other apps when enabled.

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Action] Natively - Open PDF

* **URL** - the public URL of the PDF file to display. Either URL or Base64 must be provided.
* **Base64** - the Base64 encoded content of the PDF file. Either URL or Base64 must be provided.
* **File name** - optional. The name to use when saving or sharing the file (e.g. `invoice_Q4.pdf`). If not provided, a default name with a timestamp will be generated (e.g. `1234567890.pdf`)
* **Download** - Yes/No. Controls whether the download and share buttons appear in the PDF viewer.

<figure><img src="/files/5NepICHMXzaom4IJ2Xyz" alt="" width="341"><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY PDF VIEWER - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — openPDF is available
// directly on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.openPDF(options, callback)
//   - Opens a PDF file using the device's native PDF viewer.
//   - options.url: string — public URL of the PDF file. Use url OR base64, not both.
//   - options.base64: string — Base64 encoded PDF content. Use url OR base64, not both.
//   - options.fileName: string — optional file name for saving/sharing. Defaults to a timestamp (e.g. 1234567890.pdf) if not provided.
//   - options.download: boolean — shows or hides BOTH the download and share buttons together.

// ============================================================================
// CALLBACK RESPONSE FIELDS
// ============================================================================
// resp.status  - "SUCCESS" or "FAILED"
// resp.message - description of the result or error

// --- PDF Viewer. Open from URL Example. Start ---

window.natively.openPDF({
    url: 'https://example.com/documents/guide.pdf',
    fileName: 'UserGuide.pdf',
    download: true
}, (response) => {
    console.log('PDF Viewer action:', response);
});

// --- PDF Viewer. Open from URL Example. End ---


// --- PDF Viewer. Open from Base64 Example. Start ---

window.natively.openPDF({
    base64: 'JVBERi0xLjMKJcTl8uXrp...', // Truncated for brevity
    fileName: 'GeneratedReport.pdf',
    download: false
}, (response) => {
    if (response.status === 'FAILED') {
        console.error('PDF Viewer action:', response.message);
    }
});

// --- PDF Viewer. Open from Base64 Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively PDF Viewer SDK: // window.natively.openPDF(options, callback) — opens a PDF file using the device's native PDF viewer. options.url: string — public URL of the PDF file. options.base64: string — Base64 encoded PDF content. Use either url or base64, not both. options.fileName: string — optional file name for saving/sharing, if not provided a default name with timestamp is generated (e.g. 1234567890.pdf). options.download: boolean — shows or hides BOTH the download and share buttons together (true = both shown, false = both hidden). resp.status: "SUCCESS" or "FAILED". resp.message: description of the result or error. For reference: https://docs.buildnatively.com/guides/integration/pdf-viewer
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Open a PDF invoice from a URL when the user taps the View Invoice button, with a download button enabled".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Load from URL vs Base64

Use a URL when the PDF is hosted on a publicly accessible server - this is the simplest and most common approach. Use Base64 when you need to display a dynamically generated PDF (such as an invoice or report) that isn't hosted anywhere, by encoding the file content directly and passing it to the viewer.

#### Always provide a file name

While optional, providing a meaningful file name (e.g. `invoice_2024_Q4.pdf`) makes a much better experience when the user downloads or shares the file. Without it, the file will be saved with a timestamp as the name, which is confusing for users.

`download` controls both the download and share buttons

The `download` parameter shows or hides the download and share buttons - there's no way to show one without the other. Set `download: true` for documents users are likely to want to save or send on - invoices, tickets, reports, contracts. Set `download: false` for documents that should be viewed in-app only, like onboarding guides or terms of service.

#### Download behavior

When the user taps the download button, the PDF is saved directly to the device's default Downloads folder. A snackbar notification appears at the bottom of the screen confirming the download's success or failure, and the download button is replaced with a green checkmark icon indicating the file was saved.

#### Only provide a URL or Base64, not both

If you provide both `url` and `base64`, the behavior is undefined. Always use one or the other.

## Troubleshooting

<details>

<summary>PDF not loading from URL</summary>

Make sure the URL is publicly accessible - private, authenticated, or local URLs will not work. Test the URL directly in a browser before passing it to `openPDF`. Also ensure the URL points directly to a `.pdf` file, not to a page that embeds a PDF viewer.

</details>

<details>

<summary>PDF not loading from Base64</summary>

Verify the Base64 string is a valid encoding of a PDF file. The string should start with `JVBERi0` (the Base64 encoding of `%PDF-`). An incomplete or malformed Base64 string will cause the viewer to fail.

</details>

<details>

<summary><code>resp.status</code> returns <code>FAILED</code></summary>

Check `resp.message` for the specific error description. Common causes include an inaccessible URL, malformed Base64 content, or a file that is not a valid PDF.

</details>

<details>

<summary>File saved with a timestamp name instead of a meaningful name</summary>

You didn't provide a `fileName` in the options. Always pass a descriptive file name to avoid files being saved as `1234567890.pdf`.

</details>

<details>

<summary>Download or share button not appearing</summary>

Make sure `download: true` is set in the options - this single flag controls both buttons. If using the Bubble plugin, verify the Download field is set to Yes.

</details>

<details>

<summary>PDF viewer not working in a web browser</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build or the Preview app.

</details>

[^1]: Replace this placeholder


# Request User's Review

Prompt users to rate and review your app on the App Store or Google Play.

## What is Request User's Review?

Request User's Review triggers the native in-app review prompt, asking users to rate your app directly without leaving it. Both iOS and Android provide a system-managed dialog - your app doesn't control exactly when or how often it appears, as both platforms limit how frequently it can be shown to avoid annoying users.

{% hint style="warning" %}
This feature will not work until your app is published on the App Store or Google Play. It will not appear in the Natively Preview app or in development builds.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Action] Natively - Request AppStore/GooglePlay review

Triggers the native in-app review prompt. No parameters required.
{% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY REQUEST USER'S REVIEW - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — requestAppReview is available directly
// on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.requestAppReview()
//   - Triggers the native in-app review prompt.
//   - No parameters required. No callback.
//   - The system controls whether and when the dialog is actually shown.

// --- Request User's Review. Example. Start ---

window.natively.requestAppReview();

// --- Request User's Review. Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Request User's Review SDK: // window.natively.requestAppReview() — triggers the native in-app review prompt. No parameters or callback required. The system controls whether and when the dialog is actually shown. For reference: https://docs.buildnatively.com/guides/integration/request-users-review
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Show the app review prompt after the user completes their third booking".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Time it right

The most important rule is timing - show the review prompt at a moment of success, not frustration. Good moments include after a user completes a key action (finishes an order, completes a task, reaches a milestone), not immediately on app launch or mid-flow.

#### The system controls the final decision

Calling `requestAppReview()` does not guarantee the dialog will appear. Both iOS and Android limit how frequently the prompt can be shown to the same user - typically no more than 3 times per year on iOS. If the system decides not to show it, nothing happens, and no error is returned. Design your logic around this - don't depend on the prompt appearing every time.

#### Alternative: direct to the store

If you want to send users directly to the App Store review page instead of using the in-app prompt, use the [Open External App](/hidden-pages/open-an-external-app-url#open-external-app) with the following URLs:

**iOS - opens directly to the App Store review screen:**\
`itms-apps://itunes.apple.com/app/idXXXXXXXX?action=write-review`

Replace `idXXXXXXXX` with your App Store ID.

**Android - opens your app's Google Play listing:**\
`market://details?id=YOUR_PACKAGE_NAME`

Replace `YOUR_PACKAGE_NAME` with your app's Bundle ID.

{% hint style="info" %}
Note that Android does not support linking directly to the review section - users will land on the app's Play Store listing and can navigate to reviews from there.
{% endhint %}

## Troubleshooting

<details>

<summary>The review prompt doesn't appear</summary>

This is expected behavior - both iOS and Android limit how frequently the prompt can be shown. The system may suppress it if the user has already been prompted recently, has already reviewed the app, or if the platform's internal limit has been reached. You cannot override this behavior.

</details>

<details>

<summary>The review prompt never appears in testing</summary>

The feature will not work until your app is published on the App Store or Google Play. It will not appear in the Natively Preview app or in development builds. Test it only after your app is live.

</details>

<details>

<summary>The alternative store URL doesn't open the Play Store app on Android</summary>

If `market://details?id=YOUR_PACKAGE_NAME` doesn't open the Play Store app, the device may not have the Play Store installed.

</details>

<details>

<summary>The alternative iOS URL doesn't work</summary>

Make sure you are using the correct App Store ID in the URL (`itms-apps://itunes.apple.com/app/idXXXXXXXX?action=write-review`). The ID must match your app's App Store App ID, not the Bundle ID.

</details>

[^1]: Replace this placeholder


# Scanner (QR/Barcode)

Scan QR codes and barcodes directly from your app using the device's native camera.

## What is Scanner?

The Scanner feature opens a native camera interface for scanning QR codes and barcodes, returning the decoded content as text. This is useful for check-ins, product lookups, coupon redemption, adding contacts, or any workflow where users need to scan a code rather than type.

Scanner requests [camera](/hidden-pages/camera) permission on both iOS and Android. Users will be prompted to grant this permission the first time the feature is used.

Natively supports a wide range of 1D and 2D formats:

<table data-search="false"><thead><tr><th>1D product</th><th>1D industrial</th><th>2D</th></tr></thead><tbody><tr><td>UPC-A</td><td>Code 39</td><td>QR Code</td></tr><tr><td>UPC-E</td><td>Code 93</td><td>Data Matrix</td></tr><tr><td>EAN-8</td><td>Code 128</td><td>Aztec</td></tr><tr><td>EAN-13</td><td>Codabar</td><td>PDF 417</td></tr><tr><td>UPC/EAN Extension 2/5</td><td>ITF</td><td>MaxiCode</td></tr><tr><td></td><td></td><td>RSS-14</td></tr><tr><td></td><td></td><td>RSS-Expanded</td></tr></tbody></table>

{% hint style="info" %}
Barcodes with a higher symbol density require a higher-resolution camera to scan successfully.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

{% hint style="info" %}
Scanner uses the [Camera](/hidden-pages/camera) feature, which is enabled by default for all new apps. If the Camera has been disabled for your app, you'll need to re-enable it for Scanner to work.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

### \[Element] Natively - Scanner (QR/Barcode Scanner)

#### Events:

* **Scanner Result Updated** - fires when a code is successfully scanned.

#### States:

* **Scanner Result** - the decoded text content of the scanned code.

#### Actions:

* **Show Scanner** - opens the native scanner interface.
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY SCANNER (QR/BARCODE) - DOCUMENTATION & EXAMPLES
// ============================================================================

const scanner = new NativelyScanner();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// scanner.showScanner(callback)
//   - Opens the native scanner interface for QR codes and barcodes.
//   - callback: function — required.

// ============================================================================
// CALLBACK RESPONSE FIELDS - showScanner
// ============================================================================
// resp.result: string — the decoded text content of the scanned code

// --- Scanner. Show Scanner Example. Start ---

const open_scanner_callback = function (resp) {
  console.log(resp.result); // decoded text content
};

scanner.showScanner(open_scanner_callback);

// --- Scanner. Show Scanner Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Scanner SDK: const scanner = new NativelyScanner(); scanner.showScanner(callback) opens the native scanner interface for QR codes and barcodes, returning resp.result (the decoded text content). For reference: https://docs.buildnatively.com/guides/integration/scanner-qr-barcode
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Open the scanner when the user taps the scan button, and display the decoded result on the screen".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Parse the result based on your expected format

`resp.result` is always a plain string, regardless of the code type scanned - whether it's a URL, a product ID, or a plain text value. Your app is responsible for interpreting the content (e.g., checking if it starts with `https://` to decide whether to open it as a link).

#### Combine with Deep Links for smart routing

If your QR codes encode a URL pointing to your own domain, you can navigate directly within your app, depending on the content.

#### Higher-density codes need better cameras

If users report trouble scanning densely packed barcodes (e.g., PDF417 or Data Matrix with lots of encoded data), this may be a hardware limitation on lower-end devices rather than a bug in your implementation.

## Troubleshooting

<details>

<summary>Scanner opens but never detects a code</summary>

Make sure the code is in a supported format (see the table above). Proprietary or non-standard barcode formats aren't supported. Also confirm there's enough lighting and the camera has a clear, steady view of the code.

</details>

<details>

<summary>Scanning works inconsistently on some devices</summary>

Higher-density barcodes may require a higher-resolution camera. This is more likely on older or budget devices.

</details>

<details>

<summary>Not working in a web browser</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build or the Preview app.

</details>

[^1]: Replace this placeholder


# Share Media/Files

Open the native share sheet to let users share text, images, or files with other apps on their device.

## What is Share Media/Files?

Share Media/Files opens the device's native share sheet, letting users share content directly from your app to any other app that supports it - messaging apps, email, social media, cloud storage, and more. You can share plain text, an image by URL, a combination of text and image, or a file by URL.

{% hint style="info" %}
The share sheet is handled entirely by the OS - your app doesn't control which apps appear or what the user selects, and there is no callback when the user completes or dismisses the share.
{% endhint %}

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Action] Natively - Share

* **Type** - the type of content to share:
  * `text` - share plain text or a URL;
  * `image` - share an image by URL;
  * `both` - share text/URL combined with an image;
  * `file` - share a file by URL;
* **Text or URL** - the text or URL to share (used for `text` and `both` types)
* **Image URL** - the URL of the image to share (used for `image` and `both` types)
* **File URL** - the URL of the file to share (used for `file` type)
  {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY SHARE MEDIA / FILES - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — share methods are available directly
// on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.shareText(text)
//   - Opens the native share sheet with plain text or a URL.
//   - text: string — the text or URL to share
//
// window.natively.shareImage(imageUrl)
//   - Opens the native share sheet with an image.
//   - imageUrl: string — publicly accessible URL of the image to share
//
// window.natively.shareTextAndImage(text, imageUrl)
//   - Opens the native share sheet with both text and an image.
//   - text: string — the text or URL to share
//   - imageUrl: string — publicly accessible URL of the image to share
//
// window.natively.shareFile(fileUrl)
//   - Opens the native share sheet with a file.
//   - fileUrl: string — publicly accessible URL of the file to share

// --- Share Media / Files. Share Text Example. Start ---

window.natively.shareText("Check out this link: https://yourapp.com");

// --- Share Media / Files. Share Text Example. End ---


// --- Share Media / Files. Share Image Example. Start ---

window.natively.shareImage("https://yourapp.com/image.jpg");

// --- Share Media / Files. Share Image Example. End ---


// --- Share Media / Files. Share Text and Image Example. Start ---

window.natively.shareTextAndImage(
    "Check out this image!",
    "https://yourapp.com/image.jpg"
);

// --- Share Media / Files. Share Text and Image Example. End ---


// --- Share Media / Files. Share File Example. Start ---

window.natively.shareFile("https://yourapp.com/document.pdf");

// --- Share Media / Files. Share File Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the line below and paste it into your AI agent.&#x20;

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Share Media/Files SDK: // window.natively.shareText(text) — shares plain text or a URL. // window.natively.shareImage(imageUrl) — shares an image by URL. // window.natively.shareTextAndImage(text, imageUrl) — shares text and an image together. // window.natively.shareFile(fileUrl) — shares a file by URL. No callback required for any method. For reference: https://docs.buildnatively.com/guides/integration/share-media-files
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Add a share button that lets users share the current page URL with a pre-written message".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Share a referral link or URL

Use `shareText` to let users share a link to your app or a specific page. Pre-fill the text with a message and the URL so the user just picks where to send it.

#### Share a product image

Use `shareImage` or `shareTextAndImage` to let users share a product photo with a caption. Useful for e-commerce apps where users want to send items to friends.

#### Share a document or file

Use `shareFile` to let users share a PDF, invoice, receipt, or any other file. The file must be publicly accessible via URL - local files are not supported.

#### Trigger from a user interaction

Always trigger the share sheet from a user interaction like a button tap. Some browsers and OS versions may block share sheets that are triggered programmatically without a direct user gesture.

#### The OS controls the share sheet

Your app has no control over which apps appear in the share sheet or what the user selects. There is no callback when the user completes or dismisses the share sheet- design your flow accordingly.

#### Image and file URLs must be publicly accessible

The URLs passed to `shareImage`, `shareTextAndImage`, and `shareFile` must be publicly accessible - the OS fetches them directly. Private or authenticated URLs will fail silently.

## Troubleshooting

<details>

<summary>Share sheet not appearing</summary>

Make sure the share is triggered from a user interaction - like a button tap - rather than automatically on page load. The OS may block share sheets that are not initiated by a direct user gesture.

</details>

<details>

<summary>Image or file not appearing in the share sheet</summary>

The URL must be publicly accessible. Private, authenticated, or local URLs will fail silently - the share sheet may open but without the expected content. Verify the URL is accessible in a browser before passing it to the share method.

</details>

<details>

<summary>Share sheet appears, but content is missing</summary>

Check that the correct method is being used for the content type - `shareImage` for images, `shareFile` for files, `shareText` for text only, and `shareTextAndImage` for both. Passing an image URL to `shareText` will share it as a plain text string, not as an image.

</details>

<details>

<summary>Not working in a web browser</summary>

This is a native feature and will not work in a standard web browser. Test on a real device using a Natively build or the Preview app.

</details>

<details>

<summary>No confirmation that the share was completed</summary>

There is no callback for this feature - your app has no way of knowing whether the user completed the share or dismissed the sheet. Design your flow without relying on a share confirmation.

</details>

[^1]: Replace this placeholder


# SMS/Email

Open the device's native SMS or email composer with pre-filled content directly from your app.

## What are native SMS/Email?

The SMS / Email feature opens the device's native messaging or mail composer, pre-filled with the content you provide. The user can review and edit the message before sending - your app doesn't send anything automatically.

This is useful for sharing order confirmations, referral links, support requests, or any other content the user might want to send via SMS or email without leaving the app flow entirely.

![Native Email and SMS composers on iOS (similar on Android)](/files/qHT9WhUgvvAamln0DIPY)

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% hint style="info" %}
All fields are optional. Any fields left empty can be filled in manually by the user in the composer.
{% endhint %}

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Element] Natively - SMS/Email

#### Events:

* **SMS action is not allowed \[iOS]** - fires when the user has restricted SMS sending for other apps.
* **SMS sent \[iOS]** - fires when the SMS was successfully sent.
* **SMS sending failed \[iOS]** - fires when the SMS failed to send.
* **SMS sending cancelled \[iOS]** - fires when the SMS composer was closed without sending.
* **Email action is not allowed \[iOS]** - fires when the user has restricted email sending for other apps.
* **Email sent \[iOS]** - fires when the email was successfully sent.
* **Email sending failed \[iOS]** - fires when the email failed to send.
* **Email saved \[iOS]** - fires when the user saved the email as a draft.
* **Email sending cancelled \[iOS]** - fires when the email composer was closed without sending.

#### Actions:

* **Send SMS** - opens the native SMS composer:
  * Recipient - phone number to pre-fill. If left empty, a contact selector will open.
  * Body - the message text to pre-fill.
* **Send Email** - opens the native email composer:
  * Subject - the email subject to pre-fill.
  * Recipient - email address to pre-fill.
  * Body - the email body to pre-fill.
    {% endtab %}

{% tab title="JavaScript SDK" %}

```javascript
// ============================================================================
// NATIVELY SMS / EMAIL - DOCUMENTATION & EXAMPLES
// ============================================================================

// Initialize
const message = new NativelyMessage();

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// message.sendSMS(body, recipient, callback)
//   - Opens the native SMS composer pre-filled with the provided content.
//   - The user reviews and sends the message manually.
//   - body: string — the message text to pre-fill. Pass an empty string to leave blank.
//   - recipient: string — phone number to pre-fill. Pass an empty string to open contact selector.
//                         Multiple recipients supported on Android (separate with , or ;)
//   - callback: function — called after the composer is closed
//
// message.sendEmail(subject, body, recipient, callback)
//   - Opens the native email composer pre-filled with the provided content.
//   - The user reviews and sends the email manually.
//   - subject: string — the email subject to pre-fill. Pass an empty string to leave blank.
//   - body: string — the email body to pre-fill. Pass an empty string to leave blank.
//   - recipient: string — email address to pre-fill. Pass an empty string to open a blank composer.
//                         Multiple recipients supported on Android (separate with , or ;)
//   - callback: function — called after the composer is closed

// ============================================================================
// CALLBACK RESPONSE FIELDS - sendSMS
// ============================================================================
// resp.status:
//   iOS:     "SENT" / "CANCELLED" / "FAILED" / "NOT_ALLOWED"
//   Android: "SMS SENT!" (always returned regardless of actual outcome)

// ============================================================================
// CALLBACK RESPONSE FIELDS - sendEmail
// ============================================================================
// resp.status:
//   iOS:     "SENT" / "CANCELLED" / "FAILED" / "NOT_ALLOWED" / "SAVED"
//   Android: "SENT" (always returned regardless of actual outcome)

// --- SMS / Email. SMS Example. Start ---

const sms_body = "Check out this referral link: https://yourapp.com/referral";
const sms_recipient = "+1234567890"; // leave empty "" to open contact selector

const send_sms_callback = function(resp) {
    console.log("SMS status:", resp.status);
};

message.sendSMS(sms_body, sms_recipient, send_sms_callback);

// --- SMS / Email. SMS Example. End ---


// --- SMS / Email. Email Example. Start ---

const email_subject = "Your order confirmation";
const email_body = "Thank you for your order. Here are your details...";
const email_recipient = "customer@example.com"; // leave empty "" to open blank composer

const send_email_callback = function(resp) {
    console.log("Email status:", resp.status);
};

message.sendEmail(email_subject, email_body, email_recipient, send_email_callback);

// --- SMS / Email. Email Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the relevant line below and paste it into your AI agent.&#x20;

#### Natively SMS

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively SMS SDK: const message = new NativelyMessage(); // message.sendSMS(body, recipient, callback) — opens native SMS composer pre-filled with provided content. body: string — message text, empty string leaves blank. recipient: string — phone number, empty string opens contact selector. Multiple recipients on Android: separate with , or ;. resp.status — iOS: "SENT"/"CANCELLED"/"FAILED"/"NOT_ALLOWED". Android: always "SMS SENT!". For reference: https://docs.buildnatively.com/guides/integration/sms-email
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Open the native SMS composer pre-filled with a referral link when the user taps the share button".
{% endhint %}

#### Natively Email

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Email SDK: const message = new NativelyMessage(); // message.sendEmail(subject, body, recipient, callback) — opens native email composer pre-filled with provided content. subject: string — email subject, empty string leaves blank. body: string — email body, empty string leaves blank. recipient: string — email address, empty string opens blank composer. Multiple recipients on Android: separate with , or ;. resp.status — iOS: "SENT"/"CANCELLED"/"FAILED"/"NOT_ALLOWED"/"SAVED". Android: always "SENT". For reference: https://docs.buildnatively.com/guides/integration/sms-email
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Open the native email composer pre-filled with an order confirmation when the user taps contact support".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Pre-fill as much as possible

The more you pre-fill, the less friction for the user. At minimum, pre-fill the recipient and body - a user who just needs to tap send is more likely to complete the action than one who has to type everything manually.

#### Leave fields empty when appropriate

If you don't know the recipient upfront - for example, when the user wants to share something with a contact of their choice - leave the recipient empty. For SMS, this opens a contact selector; for email, it opens a blank composer.

#### Handle callback statuses carefully on Android

On Android, the callback always returns `SENT` for email and `SMS SENT!` for SMS, regardless of what actually happened - the user may have cancelled, or the message may have failed. Do not rely on the Android callback status to confirm delivery. On iOS, the statuses are accurate and can be used to trigger follow-up logic.

#### `SAVED` status on iOS email

If `resp.status` returns `SAVED`, the user saved the email as a draft rather than sending it. Handle this case in your callback if your flow depends on the email being sent.

#### No sending without user confirmation

The feature always opens the native composer - your app never sends SMS or email automatically. The user must tap send themselves.

## Troubleshooting

<details>

<summary>The SMS or email composer is not opening</summary>

Make sure the feature is triggered from a user interaction - like a button tap - rather than automatically on page load. Also, verify the SDK is fully loaded before calling the method.

</details>

<details>

<summary><code>NOT_ALLOWED</code> status on iOS</summary>

The user has restricted the app from sending SMS or email in their device settings. This is a user-controlled permission - you can prompt them to go to **Settings → \[Your App] → Allow Sending** to re-enable it.

</details>

<details>

<summary>Android callback always returns <code>SENT</code> or <code>SMS SENT!</code></summary>

This is expected behavior on Android - the callback status does not reflect the actual outcome. Do not rely on Android callback status to confirm whether the message was sent, cancelled, or failed.

</details>

<details>

<summary>Multiple recipients are not working</summary>

Multiple recipients are confirmed to work on Android by separating with `,` or `;`. If you are experiencing issues on iOS, test with a single recipient first - multiple recipient behavior on iOS has not been fully confirmed.

</details>

<details>

<summary>The email composer opens, but no email client is installed</summary>

On some Android devices, if no email client is installed, the composer will not open. This is a device-level limitation - consider showing a fallback message if your audience includes users who may not have an email app configured.

</details>

[^1]: Replace this placeholder


# Toast/Banner

Display native in-app notifications to communicate status, feedback, or alerts to your users.

## What are Toast and Banner?

Toast and Banner are two types of native in-app notifications you can use to communicate with your users without interrupting their flow.

A **Toast** is a small, rounded message that appears at the bottom of the screen for a short time before disappearing automatically - useful for brief confirmations like "Copied to clipboard" or "Saved successfully".

A **Banner** is a full-width message that slides up from the bottom of the screen. It dismisses automatically but can also be swiped away - better suited for more prominent alerts or status updates that need a bit more visibility.

## Prerequisites

{% hint style="success" %}
This feature is available in all plans. [See all plans](/getting-started/subscription-plans)
{% endhint %}

{% hint style="warning" %}
Toast is not available in the Natively [Preview](/natively-platform/preview) app - test it on a full build app. Banner is available in Preview.
{% endhint %}

## Implementation

Choose your integration method below: **Bubble.io Plugin** (No-Code), **JavaScript SDK** (Code), or **AI Agents** (for AI-powered editors like Lovable, Base44, and Replit).

### Initialization

{% tabs %}
{% tab title="Bubble.io Plugin" %}
**Check Plugin**

Before starting, verify if the Natively plugin is already installed in your Bubble project.

1. Open your Bubble editor and navigate to the Plugins tab in the left sidebar.
2. **Check Installed Plugins:** Look through your list of installed plugins for "Natively iOS & Android app builder".
   * If it IS installed: Check the version number. If an update is available (e.g., you see a button saying "Update"), click it to ensure you have the latest features and bug fixes.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FmfnSUug82IdnxOAoBrak%252Fnatively_app_builder_bubble_plugin_update.png%3Falt%3Dmedia%26token%3Dc193f69f-b03b-4be4-b80b-f34ba37ac212&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=a89e4510&#x26;sv=2" alt=""><figcaption></figcaption></figure>

* If it is NOT installed: Click the + Add plugins button, search for "Natively", and click Install.

<figure><img src="https://docs.buildnatively.com/~gitbook/image?url=https%3A%2F%2F3352617162-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252F90tV7pYflEQdiAr2VfWu%252Fuploads%252FC5rA42yQHcN1uGKFbzmF%252Fnatively_app_builder_bubble_plugin.png%3Falt%3Dmedia%26token%3Dd9706d9b-dbe8-459b-b9b3-5667648aa4b7&#x26;width=768&#x26;dpr=3&#x26;quality=100&#x26;sign=9aae2297&#x26;sv=2" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="JavaScript SDK" %}
**Check SDK**

Before writing any logic, ensure the Natively SDK is correctly installed and up-to-date in your codebase.

1. Open your project's main HTML file (or header settings) and look for the Natively script tag inside the `<head>` section.
2. Install/Update: If missing or outdated, add the following code. You can specify the SDK version in the URL (e.g., `@2.26.0`).

```javascript
<head>
  <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script>
</head>
```

{% hint style="info" %}
To ensure you are using the most up-to-date version, check the [Natively GitHub releases page](https://github.com/buildnatively/js-sdk/releases) for the latest version number.
{% endhint %}
{% endtab %}

{% tab title="AI Agents" %}
**Initialize the SDK**

AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features. Before implementing any feature, the Natively SDK must be initialized in your project.

Copy the line below and paste it into your AI agent to check and set up the Natively SDK in your project.

```
Check if the following Natively SDK script is present in the <head> of index.html. If missing or outdated, add it: <script async onload="nativelyOnLoad()" src="https://cdn.jsdelivr.net/npm/natively@2.26.0/natively-frontend.min.js"></script><script>function nativelyOnLoad() { window.natively.setDebug(true); console.log("✅ Natively SDK loaded successfully."); }</script> For reference: https://docs.buildnatively.com/guides/integration/how-to-get-started https://github.com/buildnatively/js-sdk/releases
```

{% endtab %}
{% endtabs %}

### Setup Logic

{% tabs %}
{% tab title="Bubble.io Plugin" %}

#### \[Action] Natively - Show Toast

* **Text** - the message to display in the toast.
* **Type** - `DEFAULT`, `DARK`, `ERROR`, `SUCCESS`, `WARNING`, or `MATRIX`.

#### \[Action] Natively - Show Banner

* **Title** - the main heading of the banner.
* **Description** - supporting text shown below the title.
* **Type** - `SUCCESS`, `ERROR`, or `INFO` .
  {% endtab %}

{% tab title="JavaScript SDK" %}

#### Natively Toast

```javascript
// ============================================================================
// NATIVELY TOAST - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — toast method is available
// directly on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.showAppToast(type, text)
//   - Displays a small rounded notification at the bottom of the screen.
//   - Disappears automatically after a short time.
//   - type: string — "DEFAULT" / "DARK" / "ERROR" / "SUCCESS" / "WARNING" / "MATRIX"
//   - text: string — the message to display

// --- Toast. Example. Start ---

const toast_type = "SUCCESS"; // DEFAULT, DARK, ERROR, SUCCESS, WARNING, or MATRIX
const toast_text = "Copied to clipboard";
window.natively.showAppToast(toast_type, toast_text);

// --- Toast. Example. End ---
```

#### Natively Banner

```javascript
// ============================================================================
// NATIVELY BANNER - DOCUMENTATION & EXAMPLES
// ============================================================================

// No initialization required — banner method is available
// directly on the global window.natively object.

// ============================================================================
// ALL AVAILABLE METHODS
// ============================================================================
// window.natively.showAppBanner(type, title, description)
//   - Displays a full-width notification that slides up from the bottom.
//   - Dismisses automatically or can be swiped away.
//   - type: string — "SUCCESS" / "ERROR" / "INFO"
//   - title: string — the main heading of the banner
//   - description: string — supporting text shown below the title

// --- Banner. Example. Start ---

const banner_type = "SUCCESS"; // SUCCESS, ERROR, or INFO
const banner_title = "Payment confirmed";
const banner_description = "Your order has been placed successfully.";
window.natively.showAppBanner(banner_type, banner_title, banner_description);

// --- Banner. Example. End ---
```

{% endtab %}

{% tab title="AI Agents" %}
AI-powered editors like Lovable, Base44, and Replit use the JavaScript SDK to implement Natively features.&#x20;

Copy the relevant line below and paste it into your AI agent.

#### Natively Toast

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Toast SDK: // window.natively.showAppToast(type, text) — small rounded notification at the bottom of the screen, auto-dismisses. type: "DEFAULT" / "DARK" / "ERROR" / "SUCCESS" / "WARNING". text: string — message to display. For reference: https://docs.buildnatively.com/guides/integration/toast-banner
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Show a success toast when the user copies a referral code".
{% endhint %}

#### Natively Banner

<pre><code>[<a data-footnote-ref href="#user-content-fn-1">Your feature description</a>] using the Natively Banner SDK: // window.natively.showAppBanner(type, title, description) — full-width notification that slides up from the bottom, auto-dismisses or can be swiped away. type: "SUCCESS" / "ERROR" / "INFO". title: string — heading. description: string — supporting text. For reference: https://docs.buildnatively.com/guides/integration/toast-banner
</code></pre>

{% hint style="warning" %}
Replace the placeholder at the beginning with a description of what you want to build - for example: "Show a success banner when the user completes a purchase".
{% endhint %}
{% endtab %}
{% endtabs %}

### How to use

#### Choose between Toast and Banner

Use a **Toast** for brief, low-priority confirmations that don't require the user's attention - like "Copied", "Saved", or "Sent". Use a **Banner** when the message is more important and deserves more visibility - like a payment confirmation, an error that needs acknowledgment, or a status update the user should notice.

#### Match the type to the context

Both Toast and Banner support status-based types. Use `SUCCESS` for completed actions, `ERROR` for failures or problems, and `WARNING` (Toast only) for caution states. Use `INFO` (Banner only) for neutral informational messages. Use `DEFAULT` or `DARK` (Toast only) for generic messages without a specific status.

#### Pair Toast with actions

Toasts work well as immediate feedback after a user action - show a `SUCCESS` toast after a form submit, an `ERROR` toast if something fails, or a `DEFAULT` toast for neutral confirmations like copying text.

#### Use Banner for important status updates

Banners are more prominent and better suited for messages the user should notice, even if they're mid-flow, like a completed background process, a network status change, or a payment result.

## Troubleshooting

<details>

<summary>Toast not appearing in the Natively Preview app</summary>

This is expected - Toast is not supported in [Preview](/natively-platform/preview). Build and test on a real device using a full build.

</details>

<details>

<summary>Toast or Banner not appearing at all</summary>

Make sure the SDK is fully loaded before calling the method. The most reliable approach is to trigger Toast or Banner from a user interaction - like a button tap - rather than automatically on page load.

</details>

<details>

<summary>Banner not dismissing automatically</summary>

The Banner dismisses automatically after a short time. If it appears to stay, verify you are using a valid type (`SUCCESS`, `ERROR`, or `INFO`) - an invalid type may cause unexpected behavior.

</details>

[^1]: Replace this placeholder


# Troubleshooting

In this section, you can find guides that can help troubleshoot any Natively-related issue.

<details>

<summary>Push Notification doesn't work in the Android/iOS app</summary>

The first thing you must go through this checklist:&#x20;

* [ ] If you're using the **Natively Preview** app, you must [set the 'preview' tag in plugin](/natively-platform/preview) settings. **IF NOT,** you need to leave it blank.
* [ ] [After you've enabled and set up Push, you need to rebuild your app.](/natively-platform/app-info)
* [ ] [Make sure you've enabled](/hidden-pages/onesignal-notifications#enable-push-notifications) & [set up Push for your app.](/hidden-pages/setup-one-signal-app#video-guide)
* [ ] [Double-check your bubble app setup](/natively-platform/features/notifications/onesignal-push-notifications#bubble-setup-push-example)
* [ ] [Network issue](https://documentation.onesignal.com/docs/notifications-show-successful-but-are-not-being-shown#network-issues)
* [ ] [Not targeted push (missing/wrong PlayerID)](https://documentation.onesignal.com/docs/notifications-show-successful-but-are-not-being-shown#not-targeted-in-the-push)
* [ ] [App Push Permission](https://documentation.onesignal.com/docs/notifications-show-successful-but-are-not-being-shown#app-push-permissions-disabled---device-not-subscribed)
* [ ] [Android Category](https://documentation.onesignal.com/docs/notifications-show-successful-but-are-not-being-shown#android-categories-disabled)
* [ ] [Low power mode](https://documentation.onesignal.com/docs/notifications-show-successful-but-are-not-being-shown#low-power-energy-saving)
* [ ] [Do not disturb mode](https://documentation.onesignal.com/docs/notifications-show-successful-but-are-not-being-shown#do-not-disturb-mode)
* [ ] (For iOS) [Sometimes you need to re-generate the PUSH certificate](/hidden-pages/generate-ios-push-key) and re-upload it to OneSignal
* [ ] (For Android) Sometimes, Android pushes are not coming. You need to delete the app and install it one more time. (This issue will not appear after release to Store)

</details>

<details>

<summary>Deeplinks doesn’t work in the Android/iOS app</summary>

The first thing you must go through this checklist:&#x20;

* [ ] Your custom links are **not supported** in the **Natively Preview** app, you need to build & setup your own app.
* [ ] [After you've enabled and set up Deeplinks, you need to rebuild your app.](/natively-platform/app-info)
* [ ] [Make sure you've enabled ](/natively-platform/features/deep-links)& [set up Deeplinks for your app.](/hidden-pages/setup-website-universal-links-deeplinks)
* [ ] [Double-check your bubble app setup](/hidden-pages/setup-website-universal-links-deeplinks)
* [ ] (For iOS) Validate your website [here](https://branch.io/resources/aasa-validator/)
* [ ] [(For Android) Make sure you have added assetlinks.json](https://docs.buildnatively.com/guides/pages/T9Xm2XXXBsrXFSiyT9yi#setup-your-bubble.io-website)
* [ ] Make sure file is available by 'yourdomain.com / .well-known / yourfile' (it shouldn't be anywhere else)
* [ ] Make sure you're opening a link that is associated with your domain, sometimes services like [SendGrid have a redirect URL system](https://stackoverflow.com/a/40687167)
* [ ] Sometimes Apple/Google takes up to 48h to update the associated domain
* [ ] Sometimes reinstalling the app helps (Delete app → Reboot phone → Install app)

</details>

<details>

<summary>In-App Purchases doesn’t work in the Android/iOS app</summary>

The first thing you must go through this checklist:&#x20;

* [ ] In-App Purchases are **not supported** in the **Natively Preview** app, you need to build & set up your own app.
* [ ] [After you've enabled and set up In-App purchases, you need to rebuild your app.](/natively-platform/app-info)
* [ ] [Make sure you've enabled](/natively-platform/features/purchases#how-to-set-up-purchases) & [set up In-App for your app.](/hidden-pages/setup-revenuecat-app#video-guides)
* [ ] [Double-check your bubble app setup](/hidden-pages/in-app-purchases#bubble-setup-example)
* [ ] [Check the **Latest Error** value from **Natively Purchases** element.](/hidden-pages/in-app-purchases)
* [ ] [Make sure Google/Apple products are Active / Ready to submit test](https://community.revenuecat.com/sdks-51/why-are-offerings-or-products-empty-124)&#x20;
* [ ] Sometimes Apple/Google takes up to 24h to update the purchases setup
* [ ] Sometimes reinstalling the app helps (Delete app → Reboot phone → Install app)

</details>

<details>

<summary>I have a problem with a different Native feature</summary>

If you are faced with a problem related to a different native feature, go through this checklist:

* [ ] Almost all elements in the Natively bubble plugin have a **Latest Error** state, try to check its value. Usually, it helps to describe a problem.
* [ ] Join our [community in Discord](https://discord.gg/KwtHeTAsjN) and ask your question at the #general-questions channel.
* [ ] Check out our [editor test/example app](https://bubble.io/page?type=page\&name=purchases\&id=nativelyqa\&tab=tabs-1) on bubble. It has a separate page for each native feature.

</details>

## FAQ

### I'm seeing an error related to the Natively plugin when I try to use an action.

This usually happens when the plugin can't find the element it needs to trigger the action. Here are a few things to check:

* **Visibility:** Make sure the element associated with the action is placed on the page and is **visible**.
* **Element Location:** Don't place the element inside another element that might be hidden when you need to use the action (like a floating group, popup, focus group, or repeating group).
* **Required Fields:** Ensure all the required fields for the action are filled in. If you're using dynamic data, double-check that the values are not empty.

### My app was rejected by App Store / Google Play

Often some apps are got rejected by App Store or Google Play. It's normal.

First, you need to read the reason for the rejection message and apply the action steps.

Many rejections from Apple/Google teams are related to inaccurate descriptions. It might be related to unclear permission texts, In-App purchases product descriptions, or event application descriptions in a store. Here you can find a few useful links that can help you fix the problem:

1. [Release guide](/guides/testing-and-submitting-your-app)
2. [In-App purchases rejection](https://www.revenuecat.com/docs/app-store-rejections)
3. [Google Policy violation](http://www.androidb.com/2015/11/your-app-was-rejected-for-violating-googles-policy-now-what/)<br>

### After I close the app and then open it, data in RG are not updated (Chat case)

Unfortunately, we cannot handle this since it's a bubble socket issue. Check [this](https://forum.bubble.io/t/data-on-page-not-getting-updated/183223/29) thread on the bubble forum.

### I'm seeing an error when I try to rebuild my app - new agreement is missing

<figure><img src="/files/bWtuE1aVqhigkvzqXysh" alt=""><figcaption></figcaption></figure>

Go to your [App Store Connect](https://appstoreconnect.apple.com) and follow the instructions provided in the yellow banner at the top of the page.

<figure><img src="/files/mPKR2rVs6XxTB9cNLSy3" alt=""><figcaption></figcaption></figure>

### I'm seeing an error when I try to rebuild my app - app version

<figure><img src="/files/ufgqa6jUW93AeRdNKJfg" alt=""><figcaption></figcaption></figure>

This error appears when the app version in your Natively dashboard is lower then the app version in your App Store Connect > TestFlight. Or if the current version in your TestFlight is closed for new builds. \
If the app's version in TestFlight is 1.0.0, please provide version 1.0.1 (or higher) for a new build in your Natively dashboard. \
\
You can find more about app versioning in this article: <https://semver.org/>

### Why am I seeing an error when I try to add my iOS credentials?

<figure><img src="/files/D59ia5qcjRQte3mjxo0A" alt=""><figcaption></figcaption></figure>

This error appears when some of the data you've provided is invalid or mismatched. The most common reasons for this are:

* The AuthKey either has an incorrect role (it must be App Manager), has been revoked, or is not associated with the provided Issuer ID.
* The Bundle ID you entered does not exist or is not associated with the Apple account the AuthKey is for.
* The Bundle ID is not correctly linked to the App Store App ID you provided.

Follow our guide on how to provide iOS credentials for a new app here: [iOS App](/natively-platform/app-info/ios-build)

### My app opens a new tab of the internal browser on launch

This can occur when the website's actual domain doesn't match the App URL you provided in the Natively dashboard.

For example, if you set your App URL as `https://subdomain.domain.com`, but the website redirects to `https://example.com` upon opening, the app will recognize `https://example.com` as an external website. Since this domain isn't the one you initially specified as your App URL, it will open in a new tab within the app.

To resolve this, update the App URL in your Natively dashboard to reflect the actual domain your website uses. After saving this change, rebuild your app.

{% hint style="info" %}
If you still have any issues, contact us on support chat or by email at <help@buildnatively.com>
{% endhint %}

### Why is my app screen horizontally scrollable?

<figure><img src="/files/bqlgS4wxvsfP22hlkgMe" alt="" width="230"><figcaption></figcaption></figure>

This behavior occurs because there is an element on your webpage that is wider than the device's screen width.

For example, if the screen size is 390 pixels, but an element's minimum width is set to 391 pixels, the entire page will be forced to scroll horizontally.

Solution: You need to review the elements on your page to ensure they are fully responsive and correctly sized for smaller screens. This behavior originates within your website's design, not the app.

### Why are users being logged out over time, or why do they keep seeing the login screen?

Natively does not manage user sessions, cookies, or login persistence; it simply renders your website and respects your website's settings. These issues are tied directly to the session management logic within your website.

**1. Automatic Logout (Session Expiration)**

If users are being logged out after a period of time, the issue is typically caused by your website's default session expiration settings. This needs to be adjusted within your website's settings.

**2. Persistent Login Screen**

If logged-in users keep seeing the login page on app launch:

* This happens if your app's main URL points directly to the login page.
* Solution: You must verify the user's session status on the login page. If the user is authenticated, set up an automatic redirect in your workflow to immediately send them to the home page.

### The payment provider is not returning the user to the app via the Return URL

Please verify if your payment provider supports Universal Links for redirects. Some providers, such as Stripe, do not support this functionality (see [Stripe Docs](https://docs.stripe.com/payments/mobile/accept-payment?platform=ios\&type=payment\&locale=en-GB#set-up-return-url)).

The Solution: The "Landing Page" Pattern Since standard HTTP redirects often fail to trigger Universal Links on iOS/Android, you need to create a landing page:

1. Set the payment provider's return URL to a specific page on your website (e.g., `/success` or `/cancel`).
2. On this page, place a button/link (e.g., "Return to App") that points to your Universal Link.

Note: The user must manually click this link. Browsers usually block automatic scripts attempting to open Universal Links without a user gesture.

### I'm seeing an error related to the Natively plugin when I try to use an action in Bubble.

This usually happens when the plugin can't find the element it needs to trigger the action. Here are a few things to check:

* **Visibility:** Make sure the element associated with the action is placed on the page and is **visible**.
* **Element Location:** Don't place the element inside another element that might be hidden when you need to use the action (like a floating group, popup, focus group, or repeating group).
* **Required Fields:** Ensure all the required fields for the action are filled in. If you're using dynamic data, double-check that the values are not empty.

### On iOS, the page zooms in when I tap an input field

This is a common behavior in iOS. To prevent this in Bubble, you can disable zooming in your app settings:

1. Go to your Bubble editor
2. Open the "Settings" tab
3. Click on "General"
4. Under "iOS appearance," check the box that says "Prevent the user from zooming"
5. Deploy to live to publish your changes

To prevent this in another web platform, add a CSS fix:&#x20;

```css
/* Target all common input elements to satisfy iOS accessibility guidelines */
input[type="text"],
input[type="number"],
input[type="email"],
input[type="search"],
input[type="password"],
textarea,
select {
  font-size: 16px !important;
}
```

### What if Apple or Google rejects my app?

Usually, Apple/Google's team describes the reason for rejections. If you are pretty sure it's related to the source code of a mobile app build, please get in touch with support and attach relevant screenshots. Otherwise, it can be related to your website, or some data you've filled out on the App Store Connect/Google Play app page is inappropriate. In such a case, there is no obligation on the Natively platform to issue you a rebuild or refund.

### How do I change my app (iOS or Android) Bundle ID (Bundle Identifier)?&#x20;

The Bundle ID is the unique identity of your app and is locked upon creation.

We can perform a manual reset of your Bundle ID configuration only if your app or developer account has been suspended by Apple or Google.

To request this, please email <help@buildnatively.com> and attach a screenshot of the official suspension notification from the store. The screenshot must include the App Name, Bundle ID (package name), or Account Name to verify the status.

For voluntary changes (rebranding, testing, etc.), you must create a new app in your Natively dashboard and activate a separate paid plan for it.

### Why is my app loading slow? Is Natively blocking it?

From Natively's side, we are not doing anything to block your website from loading, and it's worth noting that app startup time can take 0.5-1s.&#x20;

However, If you are using a single-page app approach and the user is logged in, the problem may lie there. In this case, we recommend splitting your app into several pages, such as combining login, signup, and onboarding on one page and settings on another. This should help improve the loading speed.

### Why does the Android back button exit my app instead of going back?

The Android back button relies on your app's navigation history. If your website updates the URL as users move between pages or sections, the back button will correctly navigate back through that history.

However, if your website uses a different navigation method that doesn't change the URL (like single-page application routing without history updates, or dynamically loading content without URL changes), the native back button won't have a "history" to follow. In such cases, it defaults to simply exiting the app.

### My document file does not display in the iframe on Android.

Unfortunately, Android WebView doesn't support iframe documents preview, you can use Natively's [PDF Viewer](/guides/integration/pdf-viewer) feature instead.


# Testing & Submitting your app

Testing and submitting a mobile app are two crucial stages in the app development process.

* [Android](#android)
* [iOS](#ios)

## Android 🤖

### Testing on your device (APK)

You can simply [install](https://www.lifewire.com/install-apk-on-android-4177185) an APK file sent to your email after your app was successful build.

### Submitting the app to Google Play (AAB)

[How to submit your app to Google Play?](https://magecomp.com/blog/submit-app-to-google-play-store/)

**Note**: If you are submitting your app using a Personal Google Developer Account, [Google requires](https://support.google.com/googleplay/android-developer/answer/14151465?hl=en) mandatory pre-launch testing.

* You must successfully pass a 14-day test period with a minimum of 12 testers in the Closed Test track before your app can be submitted for review.&#x20;

**Need Testers?**

If you require testers to fulfill this requirement, we can help. Please send your request to `help@buildnatively.com` from the email address associated with your Natively account.

## Privacy Policy Errors/Warnings

1. Advertising ID

<figure><img src="/files/1CxtVS1S5h6O596qSkds" alt=""><figcaption></figcaption></figure>

To fix this warning, go to your Google Play Console, select the app which you are trying to upload, then on the left side, go to `Policy and programs -> App content` in there, fill the `Advertising ID form`.

### CAMERA/AUDIO etc. declaration

<figure><img src="/files/cuSqpWnHcy2AcsLsRQuZ" alt=""><figcaption></figcaption></figure>

To fix this error, go to your App Page in Google Play -> Policy -> App Contact -> (Under Privacy Policy), click Start

### App Bundle is signed with the wrong key

<figure><img src="/files/xFslpd44X0wSVMVnUdUI" alt=""><figcaption></figcaption></figure>

It means that you've already uploaded an APK file of the app that was built not with Natively (Or previously created Natively app)&#x20;

To migrate your app on Natively please fill out this <https://tally.so/r/wLZexO> (And provide all requested data). If you don't have all this information, please get in touch with our support in a chat or at <help@buildnatively.com>

Migration request usually takes up to 24hrs. Be patient :)

##

## iOS 🍏

### Testflight

After you've successfully received your build in AppStoreConnect you need:

* Open your app in AppStoreConnect and go to the TestFlight tab.

![](/files/4nKPLtx9HynhsexU8xSK)

* Create a new **Internal Testing** group

![](/files/2o5C1rd0M8Yk7rZjSXj5)

* Add **Testers** to this group

![](/files/058OMDj1UKQVyhI4bu8F)

* You will receive an email from AppStoreConnect with an invite to TestFlight with a link to the app.

### App Store submission

{% embed url="<https://help.apple.com/app-store-connect/#/dev34e9bbb5a>" %}
How to publish your app?
{% endembed %}

After your build was successfully delivered to the App Store Connect and you test the app through [TestFlight](#testflight) it's to submit it to App Store.

Go to your app's page in App Store Connect, and select the **App Store** tab.

![](/files/sYiY6gtHaBOBsO9J9JTt)

You need to fill out all the required information on this page. Each field has a "(?)" button that contains a more detailed explanation. If you have any issues/questions, check out this [guide](https://blog.instabug.com/how-to-submit-app-to-app-store/).

Here are a few recommendations:

* If you have **In-App purchases** implemented, check a release [checklist](https://www.revenuecat.com/docs/launch-checklist) from RevenueCat.
* **Screenshots** - dimensions should be 1242x2688, 2688×1242, 1284×2778, or 2778×1284. Format PNG/JPEG. You can do it yourself or use such services like [this](https://www.appstorescreenshot.com/).
* **Social Login** - If your app has a login with social media (like Facebook, Google, Twitter, LinkedIn, etc.). Apple [requires](https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple) such apps to add Sign In with Apple. Of course, you can hide it during the review process, but you need to understand that it violates AppStore guidelines. All responsibility for such action is on your side.
* **App Privacy** - If you're using Push Notifications [this](https://documentation.onesignal.com/docs/apple-app-privacy-requirements) guide from One Signal team can be useful.


# Affiliate program

Recommend Natively. Earn Commissions. Save Money.

Share Natively with your audience, students, friends, and colleagues using a referral link and earn up to 30% in associate commissions from qualifying purchases.

To start earning with Natively:

1\) [Create an account](https://app.buildnatively.com/auth?type=signup)

2\) Navigate to the [Affiliate page](https://app.buildnatively.com/?tab=affiliate)

3\) Copy your referral links and share them with anyone

<figure><img src="/files/7NVwfGd8EStBpG4er6Cz" alt=""><figcaption></figcaption></figure>

4\) Monitor your referrals on the Affiliate page:

5\) Once your total earnings exceed $200.00, contact the support team at <help@buildnatively.com> to initiate a payout to your account.

### Tiers

| Level               | Payout                   | Min  |
| ------------------- | ------------------------ | ---- |
| Tier 1 (By default) | Natively credits         | $200 |
| Tier 2\*            | Cash (Stripe, Paypal...) | $500 |

\*To get Tier 2, you need to contact the support team at <help@buildnatively.com>

### Commission

| Plan               | Percentage | Length                |
| ------------------ | ---------- | --------------------- |
| Monthly            | 30%        | Monthly (up to 12 mo) |
| Yearly             | 10%        | Once                  |
| Lifetime (licence) | 10%        | Once                  |

{% hint style="warning" %}
If a referral redeems a promo code on checkout, it cannot be qualified as a referral.
{% endhint %}

#### \*Pricing and affiliate fees might be changed in the future. You will be informed.


# For Partners: Natively Brand Book.

Whether you're an experienced affiliate or an eager newcomer, this guide is your strategic ally. Decode our brand intricacies, master messaging, perfect CTAs, and immerse yourself in visual richness.

## Our Mission and Values

#### Mission:

We want to inspire people to be creators. Our mission is to empower individuals and businesses by making mobile app development accessible without requiring coding expertise. We want people to understand a new way to boost their product/service and get the best out of it.&#x20;

#### Core Values:

* Innovation: Staying ahead with cutting-edge solutions like instant web-to-app conversion.
* Accessibility: Breaking technical barriers to enable everyone to own a mobile app.
* Community: Building a collaborative ecosystem of users and developers.

***

## Brand Identity

### Logo:&#x20;

The Natively logo is a blend of simplicity and modernity. Usage should ensure it's free from obstructions, maintaining a clear space around it.

**Guidelines for Logo Usage**

✅ Do: Maintain the logo's proportions and clarity.

❌  Don't: Distort, recolor, or reconfigure the logo.

<figure><img src="/files/JkY8xcYPihHd26CakyIb" alt=""><figcaption><p>Logo Natively</p></figcaption></figure>

<figure><img src="/files/YMisTLS2nUwhNMlZRSOx" alt=""><figcaption><p>Logo Natively (Dark)</p></figcaption></figure>

<figure><img src="/files/qxlruhOGTnwq5ZHIfNdt" alt=""><figcaption><p>Logo Natively (Light)</p></figcaption></figure>

### Icons:

<div><figure><img src="/files/zPMXswCYcfMT596Vo5ke" alt=""><figcaption><p>Icon Natively (Dark)</p></figcaption></figure> <figure><img src="/files/bBSzUQFHDaZ8O1hjfK5I" alt=""><figcaption><p>Icon Natively</p></figcaption></figure> <figure><img src="/files/mOBsmJsjaFM6xIvwFPYG" alt=""><figcaption><p>Icon Natively (Light)</p></figcaption></figure></div>

### Banners:

<figure><img src="/files/x5HgBlrlJRlKyjHrQtdk" alt=""><figcaption><p>Banner Natively</p></figcaption></figure>

<figure><img src="/files/AYhleJI9X0JqLouFqEND" alt=""><figcaption><p>Banner Natively (Square)</p></figcaption></figure>

### Promo:

<div><figure><img src="/files/3Sg1Cx2YFOHHCD0hbIhM" alt=""><figcaption><p>Promo Natively (Portrait)</p></figcaption></figure> <figure><img src="/files/UbfFwolS1P7QH57lRnCi" alt=""><figcaption><p>Promo Natively (Square)</p></figcaption></figure> <figure><img src="/files/tv6TWaOeqCDvCtq3H4HZ" alt=""><figcaption><p>Promo Natively (Stories)</p></figcaption></figure> <figure><img src="/files/omazCtKqRRNWWYwW424V" alt=""><figcaption><p>Promo Natively</p></figcaption></figure></div>

### Color palette:

Our primary colors are: #1F2546 or #FFFFFF for text, #1F2546 and #FD734B for background. Secondary colors are: #57CB84, #63737A, #B1B9BD and #EBEDF6.

<figure><img src="/files/gfKPhhx0exFEvOIdMv3E" alt=""><figcaption><p>Illustrative Natively color palette</p></figcaption></figure>

### Typography:&#x20;

We rely on [Inter](https://fonts.google.com/specimen/Inter) font – bold for headings, regular for other text. These are simple yet effective in ensuring readability and elegance.

***

## Tone of Voice:

Natively's voice is knowledgeable yet approachable and friendly. We combine technical expertise with user-friendly explanations. In communications, strike a balance between being professional and accessible. We aim to be informative, approachable, and empowering in our communications. We speak directly to our users, addressing their needs, challenges, and aspirations.

### Natively's Key Messages

#### ✅ Do's:

* **Simplicity in App Building:** Emphasize how Natively simplifies the process of converting websites into mobile apps  without any coding skills, making it accessible to all.
* **Innovation and Efficiency:** Showcase Natively as an innovative solution that streamlines app development and saves time.
* **Focus on Community:** Emphasize the collaborative nature of the Natively community and how feedback shapes the product.
* **Versatility:** Highlight Natively's ability to convert various types of websites and the versatility it offers to users.
* **Security:** No need to share developer account credentials anymore.
* **Automation:** Natively app creation process is fully automated and does not require developers involvement.

#### ❌ Don'ts:

* **Avoid Overpromising:** Do not make exaggerated claims about what Natively can do. Be transparent about its capabilities.
* **Technical Jargon:** Avoid using technical jargon without clear and understandable explanations.
* **Avoid Exclusivity:** Never give the impression that Natively is only for a select group of experts.

### Natively's CTA

#### ✅ Do's:

* **Clarity:** Ensure every CTA is clear in its intent and guides users effortlessly.
* **Engage Emotionally:** Make CTAs that connect on an emotional level, like "Bring Your App Dream to Life."
* **Use Active Language:** Employ verbs that prompt action, such as "Discover," "Explore," or "Start."
* **Stay Consistent:** Ensure that the CTA leads to content that matches the user's expectations from the prompt.

#### ❌ Don'ts:

* **Avoid Being Pushy:** Don't force or rush users into decisions, give them space and time to decide.
* **Mislead:** The destination after a CTA click should align with user expectations. No clickbait.
* **Overuse:** A bombardment of CTAs can overwhelm users. Keep it focused and minimal.
* **Mix Messages:** Avoid using CTAs that could send conflicting messages or confuse the user about the desired action.

### Natively's Hashtags

#### ✅ Do’s:

* \#BuildWithNatively #Natively #NativelyForYou #NativelyUpdates #NativeFeature #NativelyCommunity  #NativelyInnovation – *Hashtags used for specifically Natively-related posts*
* \#NoCode #NoCodeAppDevelopment – *to highlight that with Natively you do not need any code*
* \#MobileAppDevelopment #MobileAppBuilding #Web2App – *highlighting the main point of Natively*
* \#BubbleApp #iOSAppDevelopment – *when highlighting the specific platform/system working with Natively*
* \#Tutorial #HowTo #Guide #Checklist – *used with the educational posts*&#x20;
* **Engage with Trends:** If there's a trending topic that aligns with Natively, use its hashtag.

#### ❌ Don’ts:

* **Over-Generalize:** Avoid overly generic tags like #app or #mobile. They get lost in the noise.
* **Spammy Use:** Avoid using too many hashtags in a single post; quality is better than quantity.

***

### 🙌 Must-Have Assets:

**High-Quality Visuals:** Ensure that all visual assets, such as images and videos, are of high quality and convey Natively's professionalism.

**Consistent Branding:** Maintain consistency in design, colors, and typography to create a strong and memorable brand identity.

**Educational Content:** Provide users with informative and educational content about how to use Natively effectively.

**Use Case Illustrations:** Include visuals that illustrate the different use cases where Natively can be applied.

### 🤩 Nice-to-Have Assets:

**User-Generated Content:** Encourage users to share their experiences and content related to Natively, fostering a community of advocates.

**Visualized Data:** Present data and statistics in graphical form, making complex information more accessible.

**Customizable Assets:** Provide customizable templates or assets that users can adapt for their own promotional materials.

***

### 🎥 Video Example

[A demonstration video](https://youtu.be/Le5aRaWFS-s?si=FxN-ApB7xFVljQuk) showcases how effortlessly one can convert their site into a mobile app using Natively.

***

## 👀 Where to find us:

#### 📲 Socials:

[Twitter](https://twitter.com/buildnatively)

[LinkedIn](https://www.linkedin.com/company/buildnatively/)

[YouTube](https://www.youtube.com/playlist?list=PLYo9EJqlVMNdtzb_rm6_6kjlDCmaoAqlO)

**👥 Our founders on LinkedIn:**

<https://www.linkedin.com/in/romanfurman>&#x20;

<https://www.linkedin.com/in/bas-andrii/>&#x20;

**👥💭 Our founders on Twitter:**

<https://twitter.com/romanfurman0>&#x20;

<https://twitter.com/bas_andrii>&#x20;

#### 🕸️ [Our website](https://www.buildnatively.com/)

#### 📚 [Our documentation](https://docs.buildnatively.com/getting-started/readme)

***

\
Our testimonials and Showcase
-----------------------------

🔎 Find the reviews and testimonials on Natively [here](https://www.trustpilot.com/review/buildnatively.com?utm_medium=trustbox\&utm_source=MicroReviewCount).&#x20;

🖼️ The showcase of the mobile apps converted with Natively on [this page.](https://www.buildnatively.com/showcase)


