Amba

Expo SDK

Expo SDK quickstart for @layers/amba-expo — config plugin, Apple/Google sign-in, push integration, and prebuild workflow. Builds on @layers/amba-react-native.

@layers/amba-expo is the Expo wrapper around @layers/amba-react-native. It re-exports the same Amba singleton, and adds an Expo config plugin that wires up the native config the SDK needs (push entitlements, URL schemes, Associated Domains, Android intent filters) at prebuild time.

Use this SDK if you're on the Expo managed workflow or building with EAS Build. For bare React Native apps without Expo modules, use @layers/amba-react-native directly.

1. Install

npx expo install @layers/amba-expo @react-native-async-storage/async-storage \
                 expo-secure-store react-native-get-random-values
npx expo install expo-notifications expo-device expo-apple-authentication expo-auth-session \
                 expo-crypto

Expo will pick the version of each peer that matches your SDK version.

Token storage

On iOS and Android builds, Amba.configure() stores the session and refresh tokens in SecureStore (Keychain on iOS, EncryptedSharedPreferences on Android) when expo-secure-store and AsyncStorage are both installed and SecureStore is available on the device. Install expo-secure-store in every app that signs users in.

Where SecureStore is missing or unavailable (web, for example), the tokenStorage option of Amba.configure() decides, and the SDK logs one warning:

tokenStorageTokens live inAfter a restart
"secure" (default)SecureStore, else memorySigned in with SecureStore, else signed out
"memory"Memory onlySigned out
"asyncStorage"Plain AsyncStorage (readable from the app's files and unencrypted backups)Signed in

Choose "asyncStorage" only when the platform has no keystore and signing in again on every launch is unacceptable.

Where SecureStore holds the tokens, it also holds the anonymous id (from @layers/amba-expo 4.0.13), which signs a purely anonymous user back in. The first launch after an upgrade moves the anonymous id there from AsyncStorage. The user id stays in AsyncStorage. Without SecureStore, the anonymous id and user id stay in AsyncStorage.

Crypto setup

Required. Hermes (Expo's default JS engine) does not ship crypto.getRandomValues. Amba uses it to generate identifiers that need to be unpredictable, such as the anonymous id that signs an anonymous user back in. Without the polyfill, Amba.configure() throws SecureRandomUnavailableError (from @layers/amba-expo 4.0.13).

Import react-native-get-random-values once, as the very first import in your app's entry file, before any other Amba imports:

// index.js or App.tsx — must be the FIRST import
import 'react-native-get-random-values';
import { Amba } from '@layers/amba-expo';

Order matters: the polyfill installs crypto.getRandomValues on the global object as a side effect of being imported. If Amba loads first, it captures the (missing) global and the polyfill never takes effect. Put the import on line 1.

A one-shot console warning fires at runtime if Amba sees the fallback path engaged — that's your signal the polyfill import is missing or out of order. See the React Native SDK crypto setup section for the full rationale.

2. Add the config plugin to app.json

{
  "expo": {
    "name": "MyApp",
    "slug": "my-app",
    "scheme": "myapp",
    "plugins": [
      [
        "@layers/amba-expo",
        {
          "projectId": "<your project id>",
          "clientKey": "<your client key>",
          "scheme": "myapp",
          "googleIosClientId": "YOUR_GOOGLE_OAUTH_IOS_CLIENT_ID"
        }
      ]
    ]
  }
}

The plugin accepts these options:

  • scheme — adds a CFBundleURLTypes entry on iOS so myapp://... deep-links route into the app, and an intent-filter on the Android main activity for the same scheme.
  • googleIosClientId — appends a reverse-DNS-ed CFBundleURLTypes entry so the Google sign-in redirect lands back in the app.
  • projectId and clientKey — accepted in the plugin options for symmetry with Amba.configure(...); runtime config is the canonical source.

For Universal Links / APS Environment / UIBackgroundModes / Google Web client wiring, configure them directly in app.json's ios.associatedDomains / ios.entitlements / android.permissions and the corresponding native config blocks.

Re-run npx expo prebuild --clean after changing plugin properties.

3. Configure the SDK at runtime

// App.tsx
import { useEffect } from 'react';
import { Amba } from '@layers/amba-expo';
import { RootNavigator } from './navigation';
 
export default function App() {
  useEffect(() => {
    void Amba.configure({
      projectId: process.env.EXPO_PUBLIC_AMBA_PROJECT_ID!,
      clientKey: process.env.EXPO_PUBLIC_AMBA_CLIENT_KEY!,
    });
  }, []);
 
  return <RootNavigator />;
}

The plugin only handles native config — runtime initialization still happens via Amba.configure({ projectId, clientKey }). Keep EXPO_PUBLIC_AMBA_PROJECT_ID and EXPO_PUBLIC_AMBA_CLIENT_KEY in your env so the same values reach both prebuild and runtime.

For EAS Build, set the same env in your eas.json:

{
  "build": {
    "production": {
      "env": {
        "EXPO_PUBLIC_AMBA_PROJECT_ID": "<your project id>",
        "EXPO_PUBLIC_AMBA_CLIENT_KEY": "<your client key>"
      }
    }
  }
}

4. Sign in with Apple

The plugin wires the entitlement; the runtime call uses expo-apple-authentication:

import * as AppleAuthentication from 'expo-apple-authentication';
import { Amba } from '@layers/amba-expo';
import { Platform } from 'react-native';
 
export function AppleButton() {
  if (Platform.OS !== 'ios') return null;
  return (
    <AppleAuthentication.AppleAuthenticationButton
      buttonType={AppleAuthentication.AppleAuthenticationButtonType.SIGN_IN}
      buttonStyle={AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
      cornerRadius={8}
      style={{ width: '100%', height: 48 }}
      onPress={async () => {
        const credential = await AppleAuthentication.signInAsync({
          requestedScopes: [
            AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
            AppleAuthentication.AppleAuthenticationScope.EMAIL,
          ],
        });
        if (credential.identityToken) {
          await Amba.auth.signInWithApple(credential.identityToken);
        }
      }}
    />
  );
}

await Amba.signInWithApple() runs the same sheet and also binds the attempt to a one-time sign-in nonce, so an identity token issued to another sign-in (a different app sharing your client id, or a different attempt) can't be exchanged without that attempt's raw nonce. It needs expo-crypto or react-native-get-random-values for the nonce. When you call signInAsync yourself, pass a nonce the same way: the SHA-256 hash to Apple, the raw value to Amba.auth.signInWithApple(token, { nonce }).

5. Sign in with Google

import { useEffect } from 'react';
import * as Google from 'expo-auth-session/providers/google';
import { Amba } from '@layers/amba-expo';
 
export function GoogleButton() {
  const [request, response, promptAsync] = Google.useIdTokenAuthRequest({
    clientId: process.env.EXPO_PUBLIC_GOOGLE_OAUTH_WEB_CLIENT_ID!,
    iosClientId: process.env.EXPO_PUBLIC_GOOGLE_OAUTH_IOS_CLIENT_ID!,
    androidClientId: process.env.EXPO_PUBLIC_GOOGLE_OAUTH_ANDROID_CLIENT_ID!,
  });
 
  useEffect(() => {
    if (response?.type === 'success' && response.params.id_token) {
      void Amba.auth.signInWithGoogle(response.params.id_token);
    }
  }, [response]);
 
  return <Button title="Sign in with Google" onPress={() => promptAsync()} disabled={!request} />;
}

The plugin's googleIosClientId property keeps the iOS Google sign-in client id consistent between prebuild and runtime — set it once in app.json and reference it at runtime via env vars. (For Android, the OAuth client id comes from the Google Play Console; you don't need a plugin field.)

6. Push integration

import * as Notifications from 'expo-notifications';
import * as Device from 'expo-device';
import { Platform } from 'react-native';
import { Amba } from '@layers/amba-expo';
 
Notifications.setNotificationHandler({
  handleNotification: async () => ({
    shouldShowAlert: true,
    shouldPlaySound: true,
    shouldSetBadge: false,
  }),
});
 
export async function registerForPushNotificationsAsync() {
  if (!Device.isDevice) return null;
 
  const { status: existingStatus } = await Notifications.getPermissionsAsync();
  let finalStatus = existingStatus;
  if (existingStatus !== 'granted') {
    const { status } = await Notifications.requestPermissionsAsync();
    finalStatus = status;
  }
  if (finalStatus !== 'granted') return null;
 
  const { data: token } = await Notifications.getDevicePushTokenAsync();
  const platform = Platform.OS === 'ios' ? 'apns' : 'fcm';
  await Amba.push.register(token, platform);
  return token;
}

Call this after a successful sign-in or after the user explicitly opts in.

APNs entitlement wiring isn't in the plugin today (see § 2). Until it lands, add the APS Environment entitlement and UIBackgroundModes: ["remote-notification"] to ios.entitlements / ios.infoPlist in app.json manually — expo prebuild will pick them up from the config rather than from the plugin.

The plugin maps your scheme (e.g. myapp) into both ios/Info.plist and android/app/src/main/AndroidManifest.xml. amba's deep-link API delivers links to your app:

import { useEffect } from 'react';
import * as Linking from 'expo-linking';
 
useEffect(() => {
  const sub = Linking.addEventListener('url', ({ url }) => {
    // url === 'myapp://post/123' or 'https://myapp.com/post/123'
  });
  return () => sub.remove();
}, []);

To list deep links registered with amba server-side, query the deep-links module via the client SDK reference.

EAS Build env

For EAS Build to receive the project credentials + Google OAuth IDs at build time:

{
  "build": {
    "development": {
      "developmentClient": true,
      "distribution": "internal",
      "env": {
        "EXPO_PUBLIC_AMBA_PROJECT_ID": "<your project id>",
        "EXPO_PUBLIC_AMBA_CLIENT_KEY": "<your client key>",
        "EXPO_PUBLIC_GOOGLE_OAUTH_IOS_CLIENT_ID": "..."
      }
    },
    "production": {
      "env": {
        "EXPO_PUBLIC_AMBA_PROJECT_ID": "<your project id>",
        "EXPO_PUBLIC_AMBA_CLIENT_KEY": "<your client key>",
        "EXPO_PUBLIC_GOOGLE_OAUTH_IOS_CLIENT_ID": "..."
      }
    }
  }
}

Use the same credentials for both profiles — environment separation is handled by the server, not the client key.

4.0 surface highlights

@layers/amba-expo re-exports the full surface of @layers/amba-react-native plus the Expo-specific helpers (Amba.signInWithApple(), Amba.signInWithGoogle({...}), Amba.registerPushToken()). The shared 4.0 namespaces:

// users — by-id fetch + admin update.
await Amba.users.get('usr_abc');
await Amba.users.update('usr_abc', { display_name: 'Ada' }); // requires users:write
 
// friends — unfriend by user id.
await Amba.friends.removeFriend('usr_abc'); // NEW in 4.0
 
// flags — single-key lookup.
const flag = await Amba.flags.get('new_onboarding');
 
// sessions / sync / leagues — new namespaces.
const sessions = await Amba.sessions.list();
await Amba.sessions.revoke(sessions[0].id);
const me = await Amba.leagues.me();
 
// content — canonical names.
await Amba.content.today();
await Amba.content.library('default', { limit: 20 });
await Amba.content.item('item_abc');
 
// messaging — `conversations` is canonical.
const convos = await Amba.messaging.conversations();
 
// storage — envelope return shape.
const { data, next_cursor } = await Amba.storage.list('avatars/');

Common pitfalls

  • Forgot to re-prebuild after plugin changes — running npx expo prebuild --clean after editing app.json is mandatory. Otherwise the entitlements / Info.plist won't reflect the new config and push registration fails silently.
  • APNs key mismatch — the APNs key configured via amba_integrations_configure (provider apns) must carry the bundle_id set in app.json's ios.bundleIdentifier. If they drift, push token registration returns 200 but server-side delivery 404s.
  • Google OAuth client IDs across environments — each EAS build profile needs its own client ID matching the bundle ID for that profile. Reusing the dev client ID in production builds fails with invalid_client.
  • Plugin pinned to wrong amba SDK version — when bumping @layers/amba-expo, run npx expo install --check to align peer versions, then re-prebuild.

See also

On this page