J.Jupid Docs
Embed for partners

React Native and Expo

Embed Jupid in a React Native or Expo app with a top-level WebView.

@jupid/react-native renders Jupid in a top-level WebView. It uses the same partner identity mapping and Jupid cookie session as the browser SDK, without nested iframe storage constraints.

The package is currently source-built in the Jupid repository and is not yet published to a public package registry. Coordinate package access and staging configuration with Jupid before integrating it in a partner repository.

Prerequisites

Jupid provides the partner ID and exact HTTPS app origin, with no path or trailing slash. The partner provides an authenticated backend endpoint. The native app sends its signed-in user's partner access token to that endpoint, which returns a short-lived Jupid JWT:

{ "token": "partner-signed-jwt" }

The signing secret stays on that backend. Neither the secret nor an access or Jupid token belongs in an EXPO_PUBLIC_ variable or JavaScript bundle. See Authentication for the token claims.

The current package requires React >=19.2.3 <20, React Native >=0.86 <0.87, and React Native WebView >=13.16.1 <14. The repository example uses Expo SDK 57. Confirm those versions are compatible with your host app before adding the package supplied by Jupid.

Expo includes react-native-webview in Expo Go. Install the version compatible with the app's Expo SDK from that app's directory:

bunx expo install react-native-webview

See Expo's WebView documentation for its current bundled version.

PropRequiredPurpose
appUrlYesExact Jupid HTTPS origin, such as https://app.jupid.com
partnerIdYesPartner ID issued by Jupid
getTokenYesAsync function returning a fresh signed JWT from your backend
onErrorYesReceives token-fetch, repeated-authentication, WebView load, and external-link errors
initialPathNoRoot-relative Jupid path; defaults to /, subject to partner routing
shellNotopbar or sidebar; defaults to partner configuration
chatNoenabled or disabled; defaults to partner configuration

Minimal Expo app

The repository integration app is apps/expo-embed. It reads these public values:

EXPO_PUBLIC_JUPID_APP_URL=https://jupid-staging-app.example.com
EXPO_PUBLIC_JUPID_PARTNER_ID=your-partner-id
EXPO_PUBLIC_JUPID_TOKEN_URL=https://partner.example.com/api/jupid/embed-token

The token endpoint must authenticate the request using the partner app's normal session or access token. The complete example below provides a manual credential entry screen for integration testing. In a partner app, supply the credential from its existing sign-in flow.

import { JupidEmbed } from "@jupid/react-native"
import { type ReactNode, useState } from "react"
import { Button, StyleSheet, Text, TextInput, View } from "react-native"

const appUrl = process.env.EXPO_PUBLIC_JUPID_APP_URL
const partnerId = process.env.EXPO_PUBLIC_JUPID_PARTNER_ID
const tokenUrl = process.env.EXPO_PUBLIC_JUPID_TOKEN_URL

if (!appUrl) {
  throw new Error("Missing EXPO_PUBLIC_JUPID_APP_URL")
}
if (!partnerId) {
  throw new Error("Missing EXPO_PUBLIC_JUPID_PARTNER_ID")
}
if (!tokenUrl) {
  throw new Error("Missing EXPO_PUBLIC_JUPID_TOKEN_URL")
}

export default function App() {
  const [accessToken, setAccessToken] = useState("")
  const [credential, setCredential] = useState("")
  const [error, setError] = useState("")
  let errorView: ReactNode
  let content: ReactNode
  if (error) {
    errorView = <Text style={styles.error}>{error}</Text>
  }
  if (credential) {
    content = (
      <JupidEmbed
        appUrl={appUrl}
        partnerId={partnerId}
        getToken={async () => {
          const response = await fetch(tokenUrl, {
            headers: { Authorization: `Bearer ${credential}` },
          })
          if (!response.ok) {
            throw new Error(`Token endpoint returned HTTP ${response.status}`)
          }
          const body = (await response.json()) as { token: string }
          return body.token
        }}
        initialPath="/"
        shell="topbar"
        chat="disabled"
        onError={(nextError) => setError(nextError.message)}
      />
    )
  } else {
    content = (
      <View style={styles.connect}>
        <Text style={styles.label}>Host access credential</Text>
        <TextInput
          value={accessToken}
          onChangeText={setAccessToken}
          placeholder="Bearer access token"
          secureTextEntry
          autoCapitalize="none"
          autoCorrect={false}
          style={styles.input}
        />
        <Button
          title="Connect Jupid"
          onPress={() => {
            if (!accessToken) {
              setError("Enter a host access credential")
              return
            }
            setError("")
            setCredential(accessToken)
            setAccessToken("")
          }}
        />
      </View>
    )
  }

  return (
    <View style={styles.container}>
      {errorView}
      {content}
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  error: {
    backgroundColor: "#fee2e2",
    color: "#991b1b",
    padding: 12,
  },
  connect: {
    gap: 12,
    justifyContent: "center",
    flex: 1,
    padding: 24,
  },
  label: {
    fontSize: 16,
    fontWeight: "600",
  },
  input: {
    borderColor: "#9ca3af",
    borderRadius: 8,
    borderWidth: 1,
    padding: 12,
  },
})

The three EXPO_PUBLIC_ values are non-secret configuration. The test app keeps the interactively entered host access credential in memory, sends it only to the partner token endpoint as a bearer token, and gives Jupid only the returned short-lived JWT.

Expo host credential entry

Host credential entry before the WebView mounts.

Run the integration test

From the repository root, provide the three public values above and start the app:

bun run --filter @jupid/expo-embed start

Open it in Expo Go for the basic smoke test, or use a development build for release-equivalent links and permissions. Enter an access credential issued by the partner host and select Connect Jupid. Verify:

  1. the partner backend authenticates the bearer credential and returns { "token": "..." };
  2. all Always Bank users reach initialPath, including new and unpaid users; other partners retain their configured onboarding behavior;
  3. an invalid host credential surfaces the partner endpoint error;
  4. an invalid or expired Jupid JWT causes one new token request, while a second rejected JWT reaches onError;
  5. external top-level links and popups leave the WebView; and
  6. reloading the test app with another host user creates a fresh WebView and mapping.

Jupid onboarding in Expo

First-run Jupid onboarding inside the connected WebView.

Use a root-relative initialPath such as /money. Full URLs, // prefixes, and control characters are not accepted. For partners other than Always Bank, onboarding can override it.

Authentication lifecycle

  1. The SDK loads <appUrl>/embed/<partnerId> as the top-level WebView document with the non-secret initial header x-jupid-embed: native.
  2. Jupid selects the native bootstrap from that request header and sends the literal string jupid:ready to the SDK.
  3. The SDK calls getToken and sends JSON jupid:token with the token and navigation options to the trusted bootstrap document.
  4. The bootstrap POSTs /api/embed/session with the same native header. Jupid requires the request Origin to equal its own origin, verifies the JWT, creates the normal cookie session, and redirects within the WebView.
  5. Any JWT verification failure returns HTTP 401. The bootstrap sends the literal string jupid:auth-failed, the SDK calls getToken once more, and a second failure is reported through onError.

The current JWT is not a one-time ticket. A valid token can be replayed until its exp; Jupid requires jti but does not store or consume it. Keep the expiration short, do not persist tokens, and fetch them only for a mount or the single refresh.

The native header selects transport; it does not authenticate the user. The bearer JWT passes through the trusted Jupid bootstrap document's JavaScript before the same-origin session request. Keep appUrl fixed to the Jupid origin; the native bridge is not a general secret channel for arbitrary pages.

When the partner user changes, unmount JupidEmbed and mount a new instance with a fresh token. Do not reuse a WebView across partner users.

The bootstrap forwards authentication failures to the SDK, but other session failures, such as HTTP 403, HTTP 500, or a failed session fetch, display "Unable to open embedded workspace" inside the WebView. They do not reliably reach onError. Check the session request when that message appears; there is no automatic retry for these failures. See embed troubleshooting.

Platform boundaries

  • Expo Go and development builds. Expo Go supports this integration because it includes react-native-webview. Use an Expo development build to test the partner app's own URL schemes, universal links, permissions, and release configuration.
  • Cookies. Jupid is top-level in the WebView, so its session cookie is first-party there. Treat the WebView and system browser as separate cookie stores; do not depend on a Safari or Chrome login being shared with Jupid. The platform stores are managed through WKWebsiteDataStore and Android's CookieManager.
  • Navigation and popups. The SDK keeps top-level navigation on the exact Jupid HTTPS origin and sends external top-level URLs and popups to Linking.openURL. Embedded subframes can load other origins. New-window behavior differs by platform; React Native WebView exposes onOpenWindow and onShouldStartLoadWithRequest for explicit routing.
  • OAuth and bank providers. Open provider authorization in the system browser and return through an app or universal link. The minimal SDK does not add that callback bridge. Expo documents the system-browser and return-link pieces in expo-web-browser and Linking.
  • Files and permissions. File inputs, downloads, camera, microphone, and location need platform permissions and host handling beyond the minimal SDK. Review React Native WebView's file upload and download guidance for each enabled Jupid surface.
  • Renderer lifecycle. iOS or Android can terminate the WebView content process. Remount with a fresh token rather than assuming in-memory page state survived. The platform callbacks are onContentProcessDidTerminate and onRenderProcessGone.
  • Transport security. Production appUrl must be HTTPS. Apple App Transport Security and Android cleartext policy can reject HTTP origins; see the official Apple and Android guidance.

Test new and returning users, 401 refresh, external links, provider flows, process recovery, and user switching on physical iOS and Android devices before launch.

Continue with testing and launch for the shared environment checklist, or troubleshooting if a session fails to open.

On this page