Skip to content

curityio/react-native-haapi-sdk-demo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

React Native HAAPI Demo

Quality Availability

A reference Expo app for Curity customers integrating the HAAPI React Native SDK into their own React Native / Expo applications. Clone it, point it at your Curity Identity Server, and use it both as a working harness to explore HAAPI flows interactively and as a coding reference whose patterns you can copy into your own app.

It exercises the full Hypermedia Authentication API surface — every step type, every form-field type, RFC 7807 problems, OAuth token exchange and refresh — using a small, readable architecture intended to be lifted file-by-file.

The UI implements the Curity look and feel — a brand-themed light + dark design (Curity color tokens + Roboto, the official authenticator/brand icon set, branded Home and per-step flow screens), all driven by a small src/theme/ token layer. It stays a thin, readable presentation layer over the architecture, so the integration pattern is still the point — and the theme is yours to swap for your own design system.

📘 Official SDK documentation

The authoritative integration reference is the React Native HAAPI SDK — Quickstart on curity.io. Start there for the SDK contract; use this repo as its runnable companion to see those patterns working end-to-end.


Who this is for

  • You run a Curity Identity Server (cloud or self-hosted) and want a quick way to try HAAPI against your own clients and authenticators without writing UI from scratch.
  • You're building a React Native app that needs HAAPI and you want a worked example of the SDK contract — accessor lifecycle, discriminated-union response handling, error / retry contract, OAuth token exchange and refresh — rather than reading the SDK reference cold.
  • You want to copy patterns, not link to a library. Every file is small, deliberately uncoupled, and meant to be read.

It is not a finished consumer app. Sections of this README flag the swap-outs you'll likely make when customizing for your own product (TLS handling, persistence, theming, navigation).


What you get

  • A working HAAPI flow you can sign into with a username/password client.
  • A step engine (useReducer + command layer) that drives the flow without scattering SDK calls across the codebase.
  • A component registry with one file per HAAPI step type and one per form-field type — copy the ones you need, ignore the rest.
  • A runtime-editable Settings screen that exposes every SDK config knob (server URLs, redirect URI, client IDs, secrets, attestation, URL session / HttpURLConnection options, full auth-method matrix) — persisted to AsyncStorage and applied on demand via the Restart control in the Home header.
  • Typed error handling using the SDK's HaapiError channel, including retry-aware UI.
  • An OAuth-only refresh path (no HAAPI re-init) for token rotation.

Repository layout

.
├── README.md          ← you are here
├── AGENTS.md          ← architecture, conventions & gotchas (agents + humans)
├── CLAUDE.md          ← one-line import of AGENTS.md (for Claude Code)
├── LICENSE            ← Apache-2.0
├── start-idsvr.sh     ← deploy a local Curity Identity Server in Docker
├── stop-idsvr.sh      ← tear the deployment down
├── config/            ← docker-template.xml: the demo's HAAPI client config
└── Demo/              ← the Expo app
    ├── App.tsx
    ├── app.json       ← wires plugins, scheme, ATS, intent filters
    ├── package.json
    ├── metro.config.js
    ├── plugins/       ← two local Expo config plugins (see below)
    └── src/
        ├── config/    ← Settings types + storage + provider + SDK-config factories
        ├── navigation/← React Navigation bottom tabs (Home / Tokens / Settings)
        ├── haapi/     ← step engine: reducer + commands + types + provider
        └── components/
            ├── steps/    ← one component per StepType + registry
            ├── fields/   ← one component per FormFieldType + registry
            ├── settings/ ← form primitives + Server / iOS / Android / Auth sections
            └── ui/       ← Button / Card / Banner primitives

The HAAPI SDK is @curity/identityserver-haapi-react-native-sdk (repo: github.com/curityio/react-native-idsvr-haapi-sdk) — its source is public but it is proprietary (© Curity AB, declares UNLICENSED; Curity-maintained). Demo/package.json consumes the published 1.0.0 release from the public npm registry:

"@curity/identityserver-haapi-react-native-sdk": "^1.0.0"

npm install pulls the prebuilt package like any other dependency. To track the latest unreleased changes (or to co-develop the SDK), switch that line to the public git repo — "github:curityio/react-native-idsvr-haapi-sdk#main" — which builds the Expo module on install via its prepare script.


Prerequisites

Tool Version Notes
Node.js ≥ 22 The SDK's engines.node is >=22. Use nvm use 22.
npm ≥ 10 Bundled with Node 22.
Expo SDK / RN 56 / 0.85.3 The demo tracks the SDK's stack (Expo 56, RN 0.85.3, React 19.2.3, TypeScript 6.0.3).
Xcode ≥ 26.2 iOS only. iOS deployment target floor is 16.4 (Expo-56 requirement).
CocoaPods ≥ 1.16 iOS only. Required for Expo autolinking — even though the HAAPI native SDK is pulled via SwiftPM inside the podspec.
Android Studio + JDK 17 latest Android only. compileSdk 36, minSdk 26.
A Curity Identity Server with HAAPI enabled Default: https://localhost:8443. See "Server setup" below — note the Android-emulator caveat.

Quickstart

# 1. From the repo root, switch to Node 22 and install JS deps
cd Demo
nvm use 22
npm install

# 2. Type-check (sanity)
npm run typecheck

# 3. Generate native shells (ios/ and android/)
npx expo prebuild

# 4. iOS: install pods (also resolves the native HAAPI module via autolinking)
cd ios
pod install --repo-update
cd ..

# 5. Run on a simulator/emulator
npx expo run:ios       # ── or ──
npx expo run:android

The first run compiles the HAAPI native module and is slow (~3–5 min). Subsequent runs are incremental.

Need a Curity server? The app lands on a welcome screen but can't authenticate without one. From the repo root, ./start-idsvr.sh spins up a pre-configured Curity Identity Server in Docker — see Server setup.

Re-running expo prebuild after editing app.json will clobber manual changes in ios/ and android/. Configure native settings via app.json, not by hand.


Server setup

The demo needs a Curity Identity Server with HAAPI enabled. You have two options.

Option A — Spin up a local server (recommended)

./start-idsvr.sh (repo root) deploys the Curity Identity Server in Docker, pre-configured with the exact clients this demo connects to by default — so a fresh clone works with no edits.

Prerequisites: a Docker engine, envsubst (brew install gettext) and jq (brew install jq), and a Curity license.json copied into the repo root (grab a free community license from developer.curity.io).

What the script does, step by step — so you know what you're running:

  1. Verifies license.json is in the repo root (exits with instructions if not).
  2. Clones Curity's public curityio/mobile-deployments repo into deployment/ (the shared Docker deployment — first run only).
  3. Overlays this demo's HAAPI config (config/docker-template.xml: the two demo clients + the mobile-app-association used for passkeys) onto that deployment.
  4. Runs the Dockerized Curity Identity Server with your license.
  5. Prints the base URL to paste into the app's Settings → Base URL.

Environment variables — set these before running:

Variable Required? What it does Default
IDSVR_HOST_NAME Yes — unless USE_NGROK=true Host the server advertises as its base-url; must be reachable from your simulator/emulator. A LAN IP works for both platforms. none — script exits if unset
USE_NGROK No true exposes the server via an ngrok tunnel instead of a host/IP — reachable anywhere, both platforms. false
APPLE_TEAM_ID No — iOS passkeys only Your Apple Developer Team ID for the server's iOS app-association (<team>.<bundle>). MYTEAMID (placeholder)
APPLE_BUNDLE_ID No — iOS passkeys only Your app's bundle id for that same association. io.curity.haapidemo
# Prereq: copy your Curity license.json into the repo root first.

# Most common — advertise a LAN IP both the iOS Simulator and Android Emulator can reach:
export IDSVR_HOST_NAME="$(ipconfig getifaddr en0)"   # e.g. 192.168.1.23
./start-idsvr.sh

# Alternative — expose it over an ngrok tunnel instead (works anywhere, both platforms):
export USE_NGROK='true'
./start-idsvr.sh

# Testing iOS passkeys — also pass a Team ID + bundle id you own (real device required):
export IDSVR_HOST_NAME="$(ipconfig getifaddr en0)"
export APPLE_TEAM_ID='ABCDE12345'
export APPLE_BUNDLE_ID='com.example.haapidemo'
./start-idsvr.sh

# Stop the server when you're done:
./stop-idsvr.sh

How these values reach the server config: the script exports the variables, then the deployment runs envsubst over config/docker-template.xml — replacing the $RUNTIME_BASE_URL, $APPLE_TEAM_ID, and $APPLE_BUNDLE_ID placeholders with the environment values — before PATCHing the result onto the server via RESTCONF. The substitution runs in a child process, so the variables must be exported, not just assigned: export them first (as above), or set them inline on the same command. A plain APPLE_TEAM_ID=… on its own line (no export) won't reach envsubst.

# Inline form — the variables are exported only for this one command:
APPLE_TEAM_ID='ABCDE12345' APPLE_BUNDLE_ID='com.example.haapidemo' \
  IDSVR_HOST_NAME="$(ipconfig getifaddr en0)" ./start-idsvr.sh

Leave any of them unset and the script falls back to its defaults (MYTEAMID / io.curity.haapidemo), so the iOS app-association won't verify until you provide your own values.

When it finishes, the script prints the server's base URL. Open the Settings tab, set Base URL to that value, tap Save, then Start Authentication on the Home tab. Sign in with the pre-shipped test user demouser / Password1. Run ./stop-idsvr.sh when you're done.

Testing passkeys (WebAuthn)? Passkeys require the server to be associated with your app's signing identity:

  • iOS — set APPLE_TEAM_ID / APPLE_BUNDLE_ID (above) to a Team ID + bundle you own.
  • Android — the association uses the SHA-256 of the app's signing certificate in config/docker-template.xml; the shipped value is the one Curity uses in its public Android HAAPI demo. If your Android build is signed with a different keystore, replace it with your build's fingerprint (cd android && ./gradlew :app:signingReport, then copy the SHA-256).

For ordinary username/password sign-in (the default test user), you can ignore all of these.

⚠ Don't use localhost as the Curity base-url for cross-platform dev

localhost works in the iOS Simulator (which shares the host's network namespace) but fails inside the Android Emulator — there, localhost is the emulator itself.

You can't fix this just on the client side: HAAPI responses, OAuth redirects, and follow-up action.href / link.href values all carry the server's base-url. If the server thinks of itself as localhost:8443 and the client points at 10.0.2.2:8443, the first request works but every server-issued link is unreachable from Android.

For dev, point the Curity server's base-url at something both emulators can reach:

  • A LAN IP (e.g. https://192.168.x.x:8443 on your dev machine). Works for both emulators if they're on the same LAN. Simplest if you're not behind picky firewalls.
  • An ngrok / cloudflared / similar tunnel (https://abcd1234.ngrok.app). Works anywhere, terminates TLS at the tunnel, and survives moving between networks.

Then set the same URL on the client side via the Settings screen. Both ends agree, server-issued links Just Work.

Option B — Use your own Curity server

Point the demo at an existing server by editing the values in the Settings tab (no rebuild). Register two HAAPI clients matching the demo's defaults:

  • iOS: haapi-ios-client-emulator-secret-without-attestation
  • Android: haapi-android-client-emulator-secret-without-attestation

Both with client secret foo, redirect URI app://haapi, attestation disabled, and openid / profile scopes (adjust to taste). The same definitions live in config/docker-template.xml if you'd rather import them.

You don't need to edit code — Settings persists overrides to AsyncStorage; tap Save, then Restart flow on the Home tab to re-initialize with the new values. See the Settings screen section below. To change the defaults baked into a fresh install (for teammates cloning the repo), edit Demo/src/config/defaults.ts.

TLS / self-signed certificates

The demo is already wired to bypass TLS trust validation for dev servers with self-signed certificates. Two Expo config plugins handle this:

  • iOS: Demo/plugins/withHaapiUnsafeURLSession.js patches AppDelegate.swift to register an UnsafeURLSessionResolver on the SDK's NativeRegistry. Combined with URLSessionOption.Custom in Demo/src/config/ios.ts, all SDK network calls go through a URLSession whose delegate trusts any server.
  • Android: Demo/plugins/withHaapiUnsafeHttpUrlConnection.js writes UnsafeHttpURLConnectionResolver.kt next to MainApplication.kt and patches MainApplication.onCreate to register it. Combined with HttpUrlConnectionProviderOption.Custom in Demo/src/config/android.ts, all SDK network calls go through an HttpsURLConnection with a no-op trust manager.

Dev only. Both plugins disable certificate validation entirely — never ship a build with them enabled to production. Strip the plugin entries from app.json expo.plugins for production prebuilds, or gate them on a build configuration.

If you'd rather use real certs in dev, install your dev CA in the simulator/emulator trust store (iOS: drag the .crt onto the simulator window; Android: Settings → Security → Encryption & credentials → Install cert) and remove the two unsafe-resolver plugins from app.json.


Settings screen

The Settings screen exposes every SDK-consumable knob at runtime. To open it, tap the Settings tab in the bottom tab bar.

Editable fields

Section Fields
Server / Common base URL · authorization / token / revocation endpoint URLs · app redirect URI · isAutoRedirect · min token TTL · scopes (comma-separated)
iOS client ID · configuration name · URLSession option (default / ephemeral / custom) · useAttestation · attestation max retries
Android client ID · KeyStore alias · HttpURLConnection provider (default / custom) · useAttestation
iOS · Auth No-auth / Secret / MTLS / MTLS + key-pinning / JWT-asym / JWT-sym, with per-method sub-fields (PEM/PKCS12 filenames, key-pinning hostnames + base64 SHA-256 hashes, bundle option, 9 asymmetric + 3 symmetric algorithm choices)
Android · Auth Same matrix as iOS, with Android sub-fields (KeyStore options, KeyStore aliases, key-pinning entries)

Apply semantics

Saving persists the draft to AsyncStorage but does not automatically restart the active HAAPI session. After saving, navigate back to the main screen and tap Restart flowcommands.restart() closes the current accessor and re-initializes with the new values. This decoupling lets you batch multiple edits before paying the cost of a re-init.

Persistence + storage

Settings live in AsyncStorage under the key haapi-demo.appSettings.v1 (the .v1 suffix is a fixed storage namespace, not the schema version). AppSettings.settingsVersion: 2 guards forward-incompatible shape changes — loadSettings() structurally validates the payload, ignores any whose settingsVersion doesn't match (falling back to DEFAULT_SETTINGS in Demo/src/config/defaults.ts), and merges a valid payload over the defaults so additive fields stay populated across upgrades.

The client secret + any JWT symmetric key live in AsyncStorage plaintext. Acceptable for a teaching demo against a dev server. If you fork this for anything closer to production, swap in expo-secure-store for the secret fields.

Defaults for fresh clones

A teammate cloning this repo will boot with the values in Demo/src/config/defaults.ts until they save settings of their own. Edit that file when the canonical dev server / client config changes.

Self-signed-cert guard rail

If you flip URLSession option (iOS) or HttpURLConnection provider (Android) from custom to default while the base URL still looks like a LAN dev host (localhost, 192.168.*, 10.*, 172.*), the form shows an inline warning — the default network stack will reject the self-signed cert. Either keep the custom option (so the unsafe resolver plugins fire) or trust the cert in the simulator/emulator.


Integration guide (for your own app)

This section walks through the integration pattern the demo implements. If you're building your own HAAPI host app, copy the parts that fit and discard the rest.

1. Install the SDK

The SDK is the package @curity/identityserver-haapi-react-native-sdk (an Expo module), published as 1.0.0 on the public npm registry:

// package.json — preferred: the published release
"@curity/identityserver-haapi-react-native-sdk": "^1.0.0"
// alternative: track the public git repo's main branch (builds on install via the SDK's `prepare`)
// "@curity/identityserver-haapi-react-native-sdk": "github:curityio/react-native-idsvr-haapi-sdk#main"
npm install   # pulls the prebuilt package (git-dep path instead clones main + runs the SDK's `expo-module build`)

It resolves like any normal node_modules package, so no metro.config.js tweaks are needed — the default Expo config works:

const { getDefaultConfig } = require('expo/metro-config');
module.exports = getDefaultConfig(__dirname);

Only if you co-develop the SDK from a local checkout (file:../sdk) do you need the duplicate-package guards (watchFolders + a blockList for the checkout's nested node_modules/react*).

2. Build a configuration

The SDK takes three configs: per-platform (iosConfig, androidConfig) and a top-level RNHaapiConfiguration that wraps them.

// src/config/shared.ts
import { createHaapiConfiguration } from '@curity/identityserver-haapi-react-native-sdk';

export const APP_REDIRECT = 'app://haapi';   // must match URL scheme below

export function createAppHaapiConfiguration(iosConfig?, androidConfig?) {
  return createHaapiConfiguration({
    baseUrl: 'https://your-curity-host',
    authorizationEndpointUrl: 'https://your-curity-host/oauth/authorize',
    tokenEndpointUrl: 'https://your-curity-host/oauth/token',
    revocationEndpointUrl: 'https://your-curity-host/oauth/revoke',
    iosConfig, androidConfig,
    isAutoRedirect: false,
    minTokenTtl: 60,
  });
}

See Demo/src/config/ for the per-platform builders.

3. Register the URL scheme natively

In app.json (Expo):

{
  "expo": {
    "ios": {
      "bundleIdentifier": "your.bundle.id",
      "infoPlist": {
        "CFBundleURLTypes": [
          { "CFBundleURLName": "your.bundle.id.redirect",
            "CFBundleURLSchemes": ["app"] }
        ]
      }
    },
    "android": {
      "package": "your.bundle.id",
      "intentFilters": [
        { "action": "VIEW",
          "data": [{ "scheme": "app", "host": "haapi" }],
          "category": ["BROWSABLE", "DEFAULT"] }
      ]
    }
  }
}

The host of the redirect URI on Android matters — app://haapihost: "haapi". iOS doesn't require a host.

4. Wire the required Expo config plugins

For the build to succeed and the iOS app to launch, app.json expo.plugins needs four entries: the SDK's own SPM-embed plugin (referenced by package name — it adds IdsvrHaapiSdk.framework to the app bundle), the two custom TLS plugins (copy-pasteable JS in Demo/plugins/, referenced by relative path), and the community expo-build-properties.

{
  "expo": {
    "plugins": [
      "@curity/identityserver-haapi-react-native-sdk",
      "./plugins/withHaapiUnsafeURLSession",
      "./plugins/withHaapiUnsafeHttpUrlConnection",
      ["expo-build-properties", {
        "android": { "minSdkVersion": 26, "compileSdkVersion": 36, "targetSdkVersion": 36 },
        "ios":     { "deploymentTarget": "16.4" }
      }]
    ]
  }
}
Plugin Purpose Drop if…
@curity/identityserver-haapi-react-native-sdk The SDK's own plugin (listed by package name → its app.plugin.js). Adds the IdsvrHaapiSdk Swift Package to your iOS .xcodeproj so the framework is embedded in the app bundle; without it the app crashes at launch with Library not loaded: @rpath/IdsvrHaapiSdk.framework. Never (iOS). Requires an SDK build that ships app.plugin.js.
withHaapiUnsafeURLSession iOS TLS bypass for self-signed dev certs. Patches AppDelegate.swift to register a custom URLSession on the SDK's NativeRegistry. You trust your dev cert in the simulator keychain, or you only use prod-grade certs. Strip for production.
withHaapiUnsafeHttpUrlConnection Android TLS bypass for self-signed dev certs. Writes an UnsafeHttpURLConnectionResolver.kt next to MainApplication.kt and registers it on NativeRegistry. Same as iOS. Strip for production.
expo-build-properties Sets Android minSdkVersion: 26 / compileSdk: 36 / targetSdk: 36 and iOS deploymentTarget: 16.4 — the Expo-56 / SDK requirements. The bare app.json android.minSdkVersion keys are silently ignored, so this plugin is the only way to override Expo's defaults. Never. Required for builds against HAAPI SDK 5.4+.

The custom plugins use sentinel comments for idempotency, so npx expo prebuild can be re-run safely.

5. Drive the flow

The SDK exposes two factories — initializeForHaapi(config, logger?) returns an accessor with both a haapiManager (flow operations) and an oauthTokenManager (token operations). initializeForOAuth(config, logger?) returns just the token manager for refresh-only paths.

import { initializeForHaapi, getRNLogger, RNLogLevel } from '@curity/identityserver-haapi-react-native-sdk';

const logger = getRNLogger({ logLevel: RNLogLevel.Info, isSensitiveValueMasked: true });
const accessor = await initializeForHaapi(config, logger);

let response = await accessor.haapiManager.start();
// response.responseCategory === 'representation' | 'problem'
// response.representation.stepType === one of 14 values

The full call sequence — start → submitForm/followLink → ... → OAuthAuthorizationResponse → fetchAccessToken — is what the demo's HaapiProvider (Demo/src/haapi/HaapiProvider.tsx) wraps in a clean command interface.

6. Handle responses with a step registry

Every start() / submitForm() / followLink() returns a HaapiResponse discriminated on responseCategory and (when representation) on stepType. There are 14 step types and you need a component for each — even if some are placeholders.

The demo's pattern:

// src/components/steps/index.ts
export const stepRegistry: StepRegistry = {
  [StepType.AuthenticatorSelector]: AuthenticatorSelectorStep,
  [StepType.InteractiveForm]: InteractiveFormStep,
  // ...one entry per StepType value...
};

// FlowScreen.tsx
const StepComponent = stepRegistry[representation.stepType];
return <StepComponent step={representation} />;

StepRegistry is a mapped type that guarantees each component receives the correctly narrowed step variant. No switch, no manual narrowing.

The same trick handles form fields — see Demo/src/components/fields/.

7. Handle errors

Every SDK failure rejects the promise with a typed HaapiError:

import { isHaapiError, HaapiErrorCode } from '@curity/identityserver-haapi-react-native-sdk';

try {
  const response = await accessor.haapiManager.submitForm(action, params);
} catch (e) {
  if (isHaapiError(e)) {
    if (e.code === HaapiErrorCode.AccessorAlreadyInitialized) { /* close first */ }
    if (e.recovery?.kind === 'retryable') { /* show retry UI */ }
  }
}

Demo/src/haapi/errorFormat.ts (lifted from the SDK example) covers the common formatting and recovery-aware UI bits.

Important: server-side authentication problems (RFC 7807) come back inside HaapiResponse.responseCategory === 'problem' — they are not thrown. Same for OAuth token-endpoint errors (TokenResponse.responseType === 'error').

8. Lifecycle

Only one accessor is held at a time. Switching modes (HAAPI → OAuth-only refresh, or starting a new flow) requires accessor.close() first or you get HAAPI_ACCESSOR_ALREADY_INITIALIZED. FormAction / Link references are bound to the accessor that produced them — submitting a stale one after close throws HAAPI_FLOW_CACHE_MISS.


Architecture cheat-sheet

┌──────────────────────────────────────────────────────────┐
│  SettingsProvider (src/config/SettingsProvider.tsx)       │
│   AsyncStorage ↔ { settings, draft, hydrated,             │
│                    save, reset, discard }                 │
└──────────────────────┬───────────────────────────────────┘
                       │ useSettings()
                       ▼
┌──────────────────────────────────────────────────────────┐
│  HaapiProvider (src/haapi/HaapiProvider.tsx)             │
│                                                          │
│  ┌─ accessorRef (HaapiAccessor | null) ────────────┐     │
│  ├─ settingsRef (latest persisted settings) ───────┤     │
│  ├─ stateRef (current UiState mirror) ─────────────┤     │
│  └─ dispatch (engine reducer)                      │     │
│                                                          │
│  buildConfig() reads settingsRef.current →               │
│    createAppHaapiConfiguration(server, ios?, android?)   │
│                                                          │
│  Commands (async, wrap SDK calls, dispatch events):      │
│   • submitForm(action, params)                           │
│   • followLink(link)                                     │
│   • fetchTokens()                                        │
│   • refresh(refreshToken)                                │
│   • close() / restart()                                  │
└──────────────────────┬───────────────────────────────────┘
                       │ useHaapi() → { state, commands }
                       ▼
┌──────────────────────────────────────────────────────────┐
│  RootNavigator (bottom tabs)                             │
│   ├─ Home tab → FlowScreen                               │
│   │   ├─ HomeLanding (hero + Start Authentication)       │
│   │   │    shown while engine is uninitialized           │
│   │   ├─ StatusBar (busy / error / retry banner)         │
│   │   ├─ stepRegistry[step.stepType]  ← 14 components    │
│   │   │    └─ each step renders fields via               │
│   │   │       fieldRegistry[field.fieldType]  ← 7        │
│   │   ├─ ProblemView (RFC 7807)                          │
│   │   └─ "Flow complete" tip → Open Tokens               │
│   ├─ Tokens tab → TokenScreen                            │
│   │   ├─ TokenView (on completed state)                  │
│   │   └─ Token Actions (refresh / revoke)                │
│   └─ Settings tab → SettingsScreen                       │
│       ├─ ServerSection                                   │
│       ├─ IosSection      (+ AuthMethodSection)           │
│       ├─ AndroidSection  (+ AuthMethodSection)           │
│       └─ sticky bar [Reset] [Discard] [Save]             │
└──────────────────────────────────────────────────────────┘

The reducer (src/haapi/engine.ts) is pure — no SDK imports, no async — so it's trivially unit-testable. The command layer is the only place that calls the SDK. The Settings layer is the only place that mutates SDK config. That separation is the demo's main design point.


Common gotchas

  • iOS build succeeds, app crashes at launch with Library not loaded: @rpath/IdsvrHaapiSdk.framework@curity/identityserver-haapi-react-native-sdk isn't in your app.json plugins (or your installed SDK predates its app.plugin.js). Listing the package applies the SDK's SPM-embed plugin, which adds the framework to the app bundle; then re-run npx expo prebuild.
  • Android build fails with manifest merger failed: uses-sdk:minSdkVersion 24 cannot be smaller than version 26app.json android.minSdkVersion is silently ignored. Use expo-build-properties with minSdkVersion: 26.
  • Android INSTALL_FAILED_UPDATE_INCOMPATIBLE — the emulator has a previous build with a different keystore. Fix: adb -s <emu> uninstall io.curity.haapidemo, then re-run.
  • Self-signed HTTPS certs are not fixed by ATS exceptions alone — the unsafe URLSession/HttpURLConnection plugins are required for cert bypass. Or trust the cert in the simulator/emulator trust store (and drop the plugins).
  • Metro metro-config/src/defaults/exclusionList is no longer exported in metro-config@0.83+ — use metro-config/private/defaults/exclusionList and the ESM .default shape.
  • Swift 6 strict-import — bare import IdsvrHaapiReactNativeSdk in AppDelegate.swift fails with ambiguous implicit access level because the file already has internal import Expo. Use internal import IdsvrHaapiReactNativeSdk.
  • problemKind is a string literal, not the ProblemType enum. problem.problemKind === 'invalidInput' ✓ — problem.problemKind === ProblemType.InvalidInputProblem ✗ (that enum holds problem URIs, not discriminator values).
  • initializeForOAuth no-attestation rejection — fixed in SDK 5.5.0. Previously (≤5.4) it failed with invalidConfiguration("Unsupported Configuration … useAttestation is set to false") when neither attestation nor DCR was enabled, so standalone OAuth-only token ops needed an open HAAPI accessor's oauthTokenManager. As of 5.5.0 initializeForOAuth works for no-attestation/no-DCR configs; the demo still prefers an active accessor's manager when one exists.
  • iOS attestation requires a real device. Apple App Attest (DCAppAttestService) is unavailable on the iOS Simulator — so any flow with attestation enabled (useAttestation: true) must be tested on a physical iOS device. The demo ships useAttestation: false and the bundled server config (config/docker-template.xml) allows sign-in without attestation, so the default flow runs fine on the Simulator; the client-attestation policies there are pre-wired for future attestation testing.
  • A red use_dpop_nonce LogBox error in development is benign (iOS and Android as of SDK 5.5.0). The SDK gets a use_dpop_nonce challenge, retries with the server's new nonce, and the flow completes; the SDK just logs that recoverable retry at error level. iOS logs DPoP has been rejected … Retrying …; Android 5.5.0 (which now forwards native logs to JS) logs Failure obtaining HAAPI Access Token information … use_dpop_nonce. Demo/index.ts silences these with LogBox.ignoreLogs (anchored to use_dpop_nonce, so real failures still surface; dev-only, temporary — remove once the SDK adjusts the log level). Doesn't affect release builds.
  • Fast Refresh on the provider can leak the accessor and trigger HAAPI_ACCESSOR_ALREADY_INITIALIZED on re-mount. Reload Metro if you hit it.
  • Re-running expo prebuild clobbers manual edits to ios/ and android/. Edit app.json (or a plugin) instead. The four built-in plugins are idempotent on sentinel markers, so re-running prebuild reapplies them.
  • Don't upgrade React Native / Expo independently of the SDK — version mismatches cause silent ABI breaks at native build time.
  • localhost as the Curity base-url doesn't work cross-platform. iOS Simulator can reach the host's localhost; Android Emulator can't, and changing only the client URL doesn't help because the server issues follow-up links anchored on its own base-url. Configure the server with a LAN IP or an ngrok-style tunnel and use the same URL on the client (Settings → Base URL).
  • Saved settings outlive the binary. AsyncStorage survives app reinstalls on the same simulator (but is wiped by xcrun simctl erase and Android emulator data wipe). If you change defaults.ts and want fresh defaults to take effect, either tap Reset in the Settings screen or uninstall + reinstall the app.
  • Banner crashes on mixed text + JSX children if you build new banner instances. RN treats interpolated children as an array; bare strings outside <Text> are a fatal runtime error. The shipped Banner detects primitive children and wraps the whole subtree in <Text>. If you write a custom banner, do the same.

Customization checklist (before forking for your own app)

The demo defaults reflect a dev-friendly setup. When you adopt patterns into a real customer app, you'll typically swap or remove the following:

Concern Demo behavior Adjusting for your situation
TLS validation withHaapiUnsafeURLSession (iOS) / withHaapiUnsafeHttpUrlConnection (Android) plugins register URLSession / HttpURLConnection factories that trust any server cert, gated on the Custom URLSession / HttpURLConnection options in Settings. This is intentional — emulator + self-signed dev cert is the most common starting point, and it works out of the box. Keep as-is for emulator-based dev/testing. When you move to production builds, gate the two unsafe-* plugins behind a build configuration (or strip them) and either trust real certs via the OS or pin them via the SDK's MTLS-key-pinning auth method. The CustomDefault switch in Settings is a runtime toggle for either mode.
Client secret in Settings Plaintext in AsyncStorage. Fine for typing a dev secret. Swap in expo-secure-store for the secret field (and the JWT symmetric key) if your product needs the Settings screen at all (most don't).
Refresh-token storage The demo doesn't persist tokens — refresh / revoke run from the Tokens tab (TokenScreen), against the tokens held in memory. Persist tokens.refreshToken in expo-secure-store and call initializeForOAuth(...) on the next launch. (buildForOAuth() works for no-attestation configs as of SDK 5.5.0, so a standalone refresh path no longer needs DCR/attestation.)
Settings UI Always visible in the demo so you can experiment. Strip or gate behind a debug flag in your product; bake the resolved config into defaults.ts.
Server URLs Default to https://localhost:8443 (works on iOS Simulator; Android Emulator needs 10.0.2.2 or adb reverse). Bake your endpoints into defaults.ts; users override per-machine via Settings.
Bundle id / URL scheme io.curity.haapidemo / app://haapi. Pick your own (must match the server-side client redirect registration).
Theming Curity light + dark token layer in src/theme/ (the old ui/styles.ts palette is retired). Match your product design system.
Navigation Three-tab bottom nav (Home / Tokens / Settings). Extend with the flows your app actually has.

About the app.json plugins — besides the SDK's own SPM-embed plugin (referenced by package name; see the table above), the demo lists withHaapiUnsafeURLSession, withHaapiUnsafeHttpUrlConnection, and expo-build-properties:

  • expo-build-properties is required for any build (the SDK's minSdk / iOS deployment-target pinning) and should stay.
  • The two withHaapiUnsafe* plugins are required for the typical emulator + self-signed dev cert workflow and are why the demo runs out of the box. Don't reflexively strip them — you'll just have to rediscover why your app can't complete the TLS handshake to a self-signed dev server. For production builds, gate them via a build config or remove them and switch Settings to the Default URLSession / HttpURLConnection.

What's not in the demo (yet)

Feature Status
BankID / WebAuthn / External Browser end-to-end handling Placeholder UI only
Persisted refresh tokens (expo-secure-store for tokens) Not implemented
Secure storage for the client secret typed in Settings AsyncStorage plaintext today; swap in expo-secure-store for non-toy use
Multiple saved Settings profiles (dev / staging / prod) Single config slot
Import/export Settings as JSON Not implemented
Well-known OIDC metadata auto-fill in Settings Not implemented
Tests None — reducer is pure and test-ready
.env-based config Settings UI covers per-machine values; .env would let multiple checkouts share defaults
CI / Fastlane Not set up
Production-grade theming Curity light + dark token layer ships in src/theme/; further design-system polish is on you
DCR (Dynamic Client Registration) Unblocks standalone OAuth-only token ops (Tokens tab) when no HAAPI session is active
Production-safe TLS handling Unsafe-cert plugins must be stripped/gated before shipping

See AGENTS.md for architecture notes and the in-repo deferred list.


Tutorials & more information


License

The demo code is licensed under Apache-2.0 (see LICENSE). The HAAPI React Native SDK it consumes is a separate work: © Curity AB, source-available but proprietary (its source is public; it declares UNLICENSED and is governed by Curity's own license) — see the @curity/identityserver-haapi-react-native-sdk package and its repo at github.com/curityio/react-native-idsvr-haapi-sdk.

About

React native HAAPI SDK Demo App

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages