iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Native Modules

A native module is a TypeScript/JavaScript surface backed by Kotlin/Swift code. Reach for one when the JS bridge cannot reach the platform feature you need (StoreKit, Bluetooth, native camera filters, accelerated maths). Expo dev clients + Turbo Modules (the new architecture) make this less painful than the old bridge era.

A TurboModule, exposed to JS, on iOS and Android

EXAMPLE
// ===== 1) Define the JS interface (codegen target) =====
// src/specs/NativeBiometrics.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  isAvailable(): Promise<boolean>;
  authenticate(reason: string): Promise<{ ok: boolean; error?: string }>;
  getConstants(): { biometryType: 'TouchID' | 'FaceID' | 'Fingerprint' | 'None' };
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeBiometrics');

// ===== 2) Register in package.json so codegen runs =====
// {
//   "codegenConfig": {
//     "name": "NativeBiometricsSpec",
//     "type": "modules",
//     "jsSrcsDir": "src/specs"
//   }
// }

// ===== 3) Android (Kotlin) implementation =====
// android/src/main/java/com/shop/NativeBiometricsModule.kt
package com.shop

import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import com.facebook.react.bridge.*
import com.facebook.react.module.annotations.ReactModule
import java.util.concurrent.Executors

@ReactModule(name = NativeBiometricsModule.NAME)
class NativeBiometricsModule(reactContext: ReactApplicationContext) :
    NativeBiometricsSpec(reactContext) {

  companion object { const val NAME = "NativeBiometrics" }
  override fun getName() = NAME

  override fun getTypedExportedConstants(): Map<String, Any> {
    val bm = BiometricManager.from(reactApplicationContext)
    val type = if (bm.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
        == BiometricManager.BIOMETRIC_SUCCESS) "Fingerprint" else "None"
    return mapOf("biometryType" to type)
  }

  override fun isAvailable(promise: Promise) {
    val bm = BiometricManager.from(reactApplicationContext)
    promise.resolve(
      bm.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
        == BiometricManager.BIOMETRIC_SUCCESS
    )
  }

  override fun authenticate(reason: String, promise: Promise) {
    val activity = currentActivity as? FragmentActivity
      ?: return promise.reject("NO_ACTIVITY", "missing FragmentActivity")
    val exec = Executors.newSingleThreadExecutor()
    val prompt = BiometricPrompt(activity, exec,
      object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationSucceeded(r: BiometricPrompt.AuthenticationResult) {
          promise.resolve(Arguments.createMap().apply { putBoolean("ok", true) })
        }
        override fun onAuthenticationError(code: Int, msg: CharSequence) {
          promise.resolve(Arguments.createMap().apply {
            putBoolean("ok", false); putString("error", "$code: $msg")
          })
        }
      })
    activity.runOnUiThread {
      prompt.authenticate(BiometricPrompt.PromptInfo.Builder()
        .setTitle("Authenticate")
        .setSubtitle(reason)
        .setNegativeButtonText("Cancel")
        .build())
    }
  }
}

// ===== 4) iOS (Swift) implementation =====
// ios/NativeBiometrics.swift
import LocalAuthentication
import React

@objc(NativeBiometrics)
class NativeBiometrics: NSObject, NativeBiometricsSpec {
  func getConstants() -> [String: Any] {
    let ctx = LAContext()
    var error: NSError?
    let supported = ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
    let type: String = supported ? (ctx.biometryType == .faceID ? "FaceID" : "TouchID") : "None"
    return ["biometryType": type]
  }

  @objc func isAvailable(_ resolve: @escaping RCTPromiseResolveBlock,
                         reject: @escaping RCTPromiseRejectBlock) {
    let ctx = LAContext()
    var error: NSError?
    let ok = ctx.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
    resolve(ok)
  }

  @objc func authenticate(_ reason: String,
                          resolver resolve: @escaping RCTPromiseResolveBlock,
                          rejecter reject: @escaping RCTPromiseRejectBlock) {
    let ctx = LAContext()
    ctx.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { ok, err in
      DispatchQueue.main.async {
        resolve(["ok": ok, "error": err?.localizedDescription as Any])
      }
    }
  }
}

// ===== 5) Use from JS =====
// import Biometrics from '../specs/NativeBiometrics';
// const ok = await Biometrics.isAvailable();
// const r  = await Biometrics.authenticate('Approve transfer of $50');

// ===== 6) Expo path =====
// Expo Modules API hides much of this — same shape:
//   npx create-expo-module my-biometrics
//   src/MyBiometricsModule.web.ts / .android.kt / .ios.swift
// Then 'expo install ../my-biometrics' from your app.

// ===== 7) Gotchas =====
// - New Architecture (TurboModules): codegen MUST run; check ios/build logs.
// - iOS Info.plist: NSFaceIDUsageDescription required for FaceID prompt.
// - Android: depend on androidx.biometric; activity must be FragmentActivity.
// - Threading: bridge calls happen off the UI thread; switch to UI thread for prompts.

Why it matters

Reach for Expo Modules whenever you can. They give you the same TurboModule capability with a single source-of-truth API surface, a generator for the boilerplate, and a clean local dev workflow. The bare-RN native module path is fine but historically the source of the most "works on iOS, broken on Android" reports.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Drop down to Swift/Kotlin to access platform APIs not in JS.
// Modern path: Expo Modules API.
import { requireNativeModule } from 'expo-modules-core';
const MyMod = requireNativeModule('MyMod');
MyMod.doNativeThing();
Try it Yourself »

Discussion

Loading…