Building StreakCard: A Deep Dive Into Engineering a React Native Fintech App for Gen Z

September 15, 2023

StreakCard is a fintech app built for teenagers — a prepaid card with savings, gold investing, gamification, and parental controls. Building a production fintech app in React Native meant solving problems most tutorials never cover: end-to-end encryption with native modules, runtime threat detection, over-the-air updates that don't break things, and animations smooth enough that users forget they're in a cross-platform app.

This is the story of how we built it.

The Stack at a Glance

React Native (Hermes Engine) ├── State Management: Redux ├── Navigation: React Navigation (Stack + Tab + Shared Element) ├── Animations: Reanimated 3 + Lottie + Rive + Skia ├── Security: FreeRASP + JailMonkey + SSL Pinning + ECDH Native Modules ├── OTA Updates: Microsoft CodePush via AppCenter ├── Error Tracking: Sentry ├── Analytics: Firebase + MoEngage ├── Image Loading: FastImage with preload wrappers ├── Push Notifications: Firebase Cloud Messaging ├── Remote Config: Firebase Remote Config ├── Deep Linking: Firebase Dynamic Links └── Encrypted Storage: react-native-encrypted-storage

Security: Defense in Depth

When you're building an app that holds real money for minors, security isn't a feature — it's the foundation. We implemented seven layers of security that work together.

Layer 1: Jailbreak & Root Detection (JailMonkey)

The very first line of defense runs before the app even mounts. In index.js:

import JailMonkey from "jail-monkey"; import { DeviceRooted } from "@app/screens-v2/utilities/DeviceRooted"; AppRegistry.registerComponent(appName, () => JailMonkey.isJailBroken() && !__DEV__ ? DeviceRooted : App, );

If the device is rooted or jailbroken in production, we don't render the app at all. Instead, users see a security warning screen. The detection is logged to Sentry for monitoring:

Sentry.captureException(new Error("Jail Monkey Detected Device as Rooted"));

Layer 2: Runtime Application Self-Protection (FreeRASP)

JailMonkey catches the obvious cases. FreeRASP by Talsec goes deeper — it's a RASP (Runtime Application Self-Protection) SDK that continuously monitors for threats while the app is running.

We built a custom hook useFreeRaspUILayer that checks for 11 distinct threat vectors:

// rsapcheck.ts export const commonChecks = [ { name: "Privileged Access", status: "ok" }, // Root/Jailbreak { name: "Debug", status: "ok" }, // Debugger attached { name: "Simulator", status: "ok" }, // Running on emulator { name: "App Integrity", status: "ok" }, // APK/IPA tampered { name: "Unofficial Store", status: "ok" }, // Sideloaded { name: "Hooks", status: "ok" }, // Frida/XPosed detected { name: "Device Binding", status: "ok" }, // Device integrity { name: "Secure Hardware Not Available", status: "ok" }, { name: "Passcode", status: "ok" }, // No device lock set ]; export const iosChecks = [ { name: "Device ID", status: "ok" }, { name: "Passcode Change", status: "ok" }, ];

The hook initializes FreeRASP with our app's certificate hash and bundle ID:

const config = { androidConfig: { packageName: "com.streakcard", certificateHashes: ["<REDACTED_CERTIFICATE_HASH>"], }, iosConfig: { appBundleId: "com.streak.app", appTeamId: "<REDACTED_TEAM_ID>", }, watcherMail: "technical@streakcard.com", isProd: true, };

When any threat is detected, the check flips to 'nok' and we report the vulnerability matrix to Sentry:

React.useEffect(() => { let vulnerabilities = appChecks .filter((item) => item.status == "nok") .join(", "); Sentry.captureException(new Error(`Device Blocked ${vulnerabilities}`)); }, [apiOptionsJsonString]);

One interesting challenge: FreeRASP bundles SpongyCastle internally, which conflicted with our own SpongyCastle dependency for ECDH encryption. We solved this with a patch-package patch that excludes the conflicting dependency from FreeRASP's transitive dependencies.

Layer 3: SSL Certificate Pinning

We pin certificates on both platforms to prevent MITM attacks, even if the device's CA store is compromised.

Android uses the native network security config:

<domain-config cleartextTrafficPermitted="false"> <domain includeSubdomains="true">api.example.com</domain> <pin-set> <pin digest="SHA-256"><REDACTED_PIN_HASH_1></pin> <pin digest="SHA-256"><REDACTED_PIN_HASH_2></pin> <pin digest="SHA-256"><REDACTED_PIN_HASH_3></pin> </pin-set> </domain-config>

iOS uses TrustKit, initialized in AppDelegate.mm:

static void SslPinningInitialization() { NSDictionary *trustKitConfig = @{ kTSKSwizzleNetworkDelegates: @YES, kTSKPinnedDomains: @{ @"api.example.com" : @{ kTSKIncludeSubdomains: @YES, kTSKEnforcePinning: @YES, kTSKPublicKeyHashes : @[ @"<REDACTED_PIN_HASH_1>", @"<REDACTED_PIN_HASH_2>", @"<REDACTED_PIN_HASH_3>" ] } } }; [TrustKit initSharedInstanceWithConfiguration:trustKitConfig]; }

We pin three certificates to handle rotation — if one expires, the backup pins keep the app functional while we release a certificate update.

Layer 4: Biometric Authentication

The app supports fingerprint, Face ID, and device passcode fallback. Our biometricAuth.tsx utility handles the full lifecycle:

export const checkAndCreateBiomtericKey = (payload: string) => { return rnBiometrics.isSensorAvailable().then((resultObject) => { const { available } = resultObject; if (available) { return rnBiometrics.biometricKeysExist().then(async (resultObject) => { const { keysExist } = resultObject; if (keysExist) { return await createSignature(payload); } else { return await createKeys(payload); } }); } else { return Promise.reject(BiometricFailedResponse.not_available); } }); };

The biometric system doesn't just verify identity — it generates a cryptographic signature that the server validates. This means even if someone clones the app data, they can't forge authentication without the biometric-protected key.

Layer 5: Encrypted Storage

All sensitive data (JWT tokens, PINs, user data) is stored using react-native-encrypted-storage, which uses Android's EncryptedSharedPreferences and iOS Keychain under the hood:

export class Storage { static async setItem(key: string, data: Object | string) { await EncryptedStorage.setItem( key, typeof data === "string" ? data : JSON.stringify(data), ); } }

On iOS, we also clear the Keychain on fresh installs to prevent data leakage from previous app installations:

static void ClearKeychainIfNecessary() { if ([[NSUserDefaults standardUserDefaults] boolForKey:@"HAS_RUN_BEFORE"] == NO) { NSArray *secItemClasses = @[ (__bridge id)kSecClassGenericPassword, (__bridge id)kSecClassInternetPassword, (__bridge id)kSecClassCertificate, (__bridge id)kSecClassKey, (__bridge id)kSecClassIdentity ]; for (id secItemClass in secItemClasses) { NSDictionary *spec = @{(__bridge id)kSecClass: secItemClass}; SecItemDelete((__bridge CFDictionaryRef)spec); } } }

Layer 6: RSA Encryption for Sensitive Payloads

Beyond ECDH for card operations, we use RSA for certain API payloads:

import { RSA } from "react-native-rsa-native"; export class RSAEncryption { public static async Encrypt(message: string): Promise<string> { return await RSA.encrypt(message, SHA256DigestPublic); } }

Layer 7: ProGuard + Hermes Bytecode

On Android release builds, we enable full ProGuard/R8 obfuscation:

buildTypes { release { debuggable false minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } }

Combined with Hermes engine (which compiles JavaScript to bytecode), the source code is double-obfuscated — making reverse engineering significantly harder.

Custom Native Modules for Cryptography

The crown jewel of our security architecture is the end-to-end encryption layer built with custom native modules. Card transaction data is encrypted before it leaves the device using ECDH key exchange + AES symmetric encryption.

Android (Kotlin)

class CryptoReactNative : ReactContextBaseJavaModule { private val algorithmECDH = "ECDH" private val primeCurve = "prime256v1" @ReactMethod fun generateKeys(promise: Promise) { Security.removeProvider("BC") Security.addProvider(BouncyCastleProvider()) val kpg = KeyPairGenerator.getInstance(algorithmECDH) kpg.initialize(ECGenParameterSpec(primeCurve)) val keyPair = kpg.generateKeyPair() // Return as hex-encoded strings via Promise } @ReactMethod fun encryptData(serverPublicKey: String, localPrivateKey: String, value: String, promise: Promise) { val keyAgreement = KeyAgreement.getInstance(algorithmECDH) keyAgreement.init(privateKey) keyAgreement.doPhase(publicKey, true) val secretKey = keyAgreement.generateSecret(algorithmECDH) promise.resolve(getEncryptData(secretKey, value)) } }

iOS (Swift + Objective-C Bridge)

@objc(CryptoReactNativeIOS) class CryptoReactNativeIOS: NSObject { let keys = try? CC.EC.generateKeyPair(256) @objc func encrypt(_ valueToEncrypt: String, serverPublicKey: String, privateKey: String, resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) { let serverPublicKeyData = dataWithHexString(hex: serverPublicKey) let sharedSecret = try? CC.EC.computeSharedSecret( keys!.0, publicKey: serverPublicKeyData ) resolve(valueToEncrypt.aes_Encrypt( AES_KEY: sharedSecret!.withUnsafeBytes(Array.init) )) } }

JavaScript Usage (Platform Branching)

const encrypted = Platform.OS === "ios" ? await NativeModules.CryptoReactNativeIOS.encrypt( JSON.stringify(data), serverPublicKey, privateKey, ) : await CryptoReactNative.encryptData( serverPublicKey, privateKey, JSON.stringify(data), );

Over-the-Air Updates with Microsoft CodePush

In fintech, you can't wait a week for App Store review when there's a critical bug. Microsoft CodePush lets us push JavaScript bundle updates instantly to users.

Configuration

// App.tsx let CodePushOptions = { checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME, installMode: CodePush.InstallMode.IMMEDIATE, mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE, }; export default CodePush(CodePushOptions)(Sentry.wrap(App));

We check for updates every time the app comes to foreground (ON_APP_RESUME), and install immediately — no restart required.

Version Tracking

Each CodePush release gets a distribution version tied to the native binary:

export const CodepushUtils = { CODEPUSH_DIST: Platform.OS === "android" ? "v69" : "v46", VERSION_NAME: Platform.OS === "android" ? "19.2.9" : "1.90.6", };

This feeds into Sentry for accurate stack trace mapping:

Sentry.init({ release: `${BUNDLE_ID}@${NATIVE_VERSION}+codepush@${CODEPUSH_DIST}`, dist: CODEPUSH_DIST, attachStacktrace: true, });

Update Experience

We built a custom CodepushModal component that shows download progress with a Lottie animation:

class CodepushModal extends Component { codePushStatusDidChange(syncStatus) { switch (syncStatus) { case CodePush.SyncStatus.DOWNLOADING_PACKAGE: this.setState({ syncMessage: "Downloading Update." }); break; case CodePush.SyncStatus.INSTALLING_UPDATE: this.setState({ syncMessage: "Installing update." }); break; } } codePushDownloadDidProgress({ receivedBytes, totalBytes }) { const downloadProgress = (receivedBytes / totalBytes) * 100; this.setState({ progress: downloadProgress }); } render() { return ( <Modal isVisible={isVisible}> <LottieView progress={Math.round(this.state.progress) / 100} source={require("./download.json")} /> <GeneralText text={this.state.syncMessage} /> </Modal> ); } }

Deployment Pipeline

{ "bundle:codepush:android": "appcenter codepush release-react -a StreakCard/Streak-Android -d Staging --sourcemap-output --output-dir ./build", "bundle:codepush:ios": "appcenter codepush release-react -a StreakCard/Streak-Ios -d Staging --sourcemap-output --output-dir ./build" }

We also upload source maps to Sentry after each CodePush release to maintain debuggable crash reports.

Animations: Making It Feel Native

A fintech app for teenagers needs to feel alive. We used five different animation systems depending on the use case.

React Native Reanimated (35+ files)

Reanimated 3 is our primary animation library. We use it for everything from entrance animations to complex gesture-driven interactions.

Staggered List Animations — Items fade and slide in with index-based delays:

<Animated.View entering={FadeInDown.duration(600).delay(index * 150)}> <FeatureCard data={item} /> </Animated.View>

Gold Price Pulsing Dot — A live indicator that pulses infinitely on the gold price graph:

const animatedGoldDotValue = useSharedValue(0); useEffect(() => { animatedGoldDotValue.value = withRepeat( withTiming(1, { duration: 1000 }), -1, true, ); }, []); const dotStyle = useAnimatedStyle(() => ({ transform: [ { scale: interpolate(animatedGoldDotValue.value, [0, 1], [0.5, 1.5]), }, ], opacity: interpolate(animatedGoldDotValue.value, [0, 1], [1, 0.3]), }));

Success Page Multi-Step Animation — Image scales in, then the success sheet slides up from the bottom with a delayed reveal:

const fadeInAnimation = useSharedValue(0); const animatedValue = useSharedValue(0); useEffect(() => { RNReactNativeHapticFeedback.trigger("clockTick"); fadeInAnimation.value = withTiming(1, { duration: 700 }); animatedValue.value = withDelay( 1500, withTiming(1, { duration: 600, easing: Easing.ease }), ); }, []); const imageStyle = useAnimatedStyle(() => ({ opacity: interpolate(fadeInAnimation.value, [0, 1], [0, 1]), transform: [ { scale: interpolate(fadeInAnimation.value, [0, 0.5, 1], [1, 1.1, 1]), }, ], })); const contentStyle = useAnimatedStyle(() => ({ transform: [ { translateY: interpolate( animatedValue.value, [0, 1], [ScreenHeight - insets.bottom, -100], ), }, ], }));

Lottie Animations (15+ files)

We use Lottie for complex vector animations that would be impractical to code:

Tab Bar Icons — Each tab plays a micro-animation when selected:

return focused ? ( <AnimatedLottieView source={animationSource} autoPlay={false} loop={false} ref={(ref) => { if (focused) ref?.play(0, 30); }} /> ) : ( <Image source={icon} /> );

Rive Animations

For more interactive animations, we use Rive (which supports state machines):

Easter Egg — A hidden robot animation triggered by long press with haptic feedback:

<TouchableOpacity onLongPress={() => { Vibration.vibrate(1000); riveRef.current?.play(); }} > <Rive autoplay={false} ref={riveRef} resourceName="robot_easter" fit={Fit.Cover} /> </TouchableOpacity>

Animated SVG Backgrounds

For the savings section, we animate individual SVG elements using Reanimated's useAnimatedProps:

const dot1 = useSharedValue({ x: 53, y: 66 }); useEffect(() => { dot1.value = withRepeat( withSequence( withTiming({ x: 65, y: 69 }, { duration: 5000 }), withTiming({ x: 89, y: 66 }, { duration: 5000 }), ), -1, true, ); }, []); const AnimatedRect = Animated.createAnimatedComponent(Rect); const animatedProps1 = useAnimatedProps(() => ({ x: dot1.value.x, y: dot1.value.y, }));

Multiple dots animate in independent patterns, creating a subtle, organic floating effect.

Shared Element Transitions

We use react-navigation-shared-element for seamless transitions between screens. Elements visually morph from their position on one screen to another.

import { createSharedElementStackNavigator } from "react-navigation-shared-element"; const Stack = createSharedElementStackNavigator(); // Source screen <SharedElement id="Full-KYC-Icon"> <Image source={FullKYCIcon} /> </SharedElement> <SharedElement id="GoldCard"> <Image source={GoldCard} /> </SharedElement> <SharedElement id="CreateAccountButton"> <Button /> </SharedElement>

We use shared transitions for KYC icons, gold cards, onboarding buttons, and landing page descriptions — creating a sense of spatial continuity as users navigate through flows.

Skia-Powered UI: Squircles and Animated Gradients

One of our signature UI elements is the Squircle — the smooth, superellipse-shaped container that Apple popularized. We render them using @shopify/react-native-skia:

const Squircle = ({ borderRadius, backgroundColor, borderSmoothing, children, maskChildren, colors, }) => { const path = useMemo(() => { return drawSquirclePath({ borderSmoothing, borderRadius, width, height, }); }, [width, height, borderRadius, borderSmoothing]); return ( <View style={style} onLayout={onLayout}> <Canvas style={StyleSheet.absoluteFill}> <Group> <Path path={path} color={backgroundColor}> {colors && ( <LinearGradient start={vec(0, 0)} end={vec(100, 256)} colors={colors} /> )} </Path> </Group> </Canvas> {children} </View> ); };

The Squircle supports customizable border radius and smoothing factor, gradient fills via Skia's LinearGradient, mask mode for clipping children to the squircle shape, and arbitrary Skia children for advanced compositions.

Animated Gradient Backgrounds

We also use Skia for full-screen animated gradient backgrounds:

const AnimatedLinearGradientBG = ({ colors, radialColors }) => { const animatedValue = useSharedValue(0); useEffect(() => { animatedValue.value = withTiming(450, { duration: 1500 }); }, []); return ( <Canvas> <Rect x={0} y={0} width={ScreenWidth} height={ScreenHeight}> <LinearGradient start={vec(0, 0)} end={vec(ScreenWidth / 2, ScreenHeight / 2)} colors={colors} /> </Rect> <Rect x={0} y={0} width={ScreenWidth} height={ScreenHeight}> <RadialGradient c={vec(200, -200)} r={animatedValue} colors={radialColors} /> </Rect> </Canvas> ); };

This creates a radial gradient that expands outward over 1.5 seconds on screen entry — a subtle but polished effect.

Custom Navigation Transitions

We built five custom screen transition interpolators instead of relying on React Navigation's defaults:

export const Fade = ({ current }) => ({ cardStyle: { opacity: current.progress }, }); export const Zoom = ({ current, next }) => ({ cardStyle: { transform: [ { scaleX: progress.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }), }, { scaleY: progress.interpolate({ inputRange: [0, 1], outputRange: [0, 1], }), }, ], }, }); export const Right = ({ current, layouts }) => ({ cardStyle: { transform: [ { translateX: current.progress.interpolate({ inputRange: [0, 1], outputRange: [layouts.screen.width, 0], }), }, ], }, });

Different flows use different transitions: onboarding uses Fade, settings uses Right, modals use Bottom, and the gold feature entry uses Zoom.

The Card Experience

The virtual card is one of the most interaction-rich components in the app.

Slide-to-Reveal Card Details

The card number, CVV, and expiry are hidden by default. Users slide the card to reveal details with a spring-physics animation:

const fadeIn = useRef(new Animated.Value(GENERAL_PADDING / 8)).current; const SlideOutAnimation = () => { Animated.spring(fadeIn, { toValue: width * 0.13 + GENERAL_PADDING - width, useNativeDriver: true, }).start(); }; const SlideIn = () => { Animated.spring(fadeIn, { toValue: GENERAL_PADDING / 8, useNativeDriver: true, }).start(); };

Card details are fetched encrypted, parsed using Cheerio (an HTML parser), and displayed only while the card is revealed.

Card Stories

We built an Instagram-like stories feature for card issuance. It includes auto-advancing progress bars with Animated.timing, swipe gesture navigation using react-native-swipe-gestures, fade transitions between story items, full-screen Rive vector animations, and carousel-based navigation with Animated.event scroll tracking.

Gold Investing Feature

The gold module lets teenagers invest in digital gold. It includes a live gold price graph with real-time data, a pulsing green dot on the current price, and a financial planning calculator:

FV = P(1 + r)^t Where: P = Investment amount r = Expected annual return t = Time horizon (1/2/3 years)

Interactive year selector with pre-calculated returns (11%/20%/32%) and popup explanations using spring-animated FadeInDown transitions.

Gamification: Streaks, Sweeps, and Jackpots

Streak Challenges

A gamification system that rewards consistent app usage. Animated notifications with Rive confetti play when tasks are completed:

<Rive resourceName="confetti" fit={Fit.Cover} />

Lucky Sweeps & Jackpots

A slot-machine game with multiple Lottie animations for spinning reels, popup modals for wins, slider components for bet selection, and loading animations during result computation.

Learn & Earn

Quiz-style educational content with gameplay mechanics, teaching teenagers financial literacy while rewarding them with in-app currency.

Architecture Decisions

Image Loading Strategy

We built a FastImagePreloadWrapper that shows a placeholder with opacity animation while the network image loads:

const AnimatedFastImage = Animated.createAnimatedComponent(FastImage); <AnimatedFastImage style={[style, { opacity: Image }]} source={source} onLoad={() => { animateImage(Image, 1); animateImage(defaultImage, 0); }} />;

Error Boundaries

A custom FallbackComponent catches crashes gracefully — showing an animated gradient background (Skia), an error message, and a "Try again" button instead of a white screen:

import ErrorBoundary from "react-native-error-boundary"; <ErrorBoundary FallbackComponent={FallbackComponent}> <App /> </ErrorBoundary>;

App Lifecycle Management

The app handles several lifecycle scenarios:

  • Force Update Modal — when the native binary is too old
  • Maintenance Modal — server-side maintenance windows
  • Shutdown Modal — graceful service discontinuation
  • Network Error Screen — offline handling
  • App Inactivity Timeout — re-authentication after 10 minutes background
  • Deep Link Queuing — deep links that arrive during PIN verification are queued and processed after auth

Custom Hooks Architecture

We built a suite of custom hooks that encapsulate complex business logic:

HookPurpose
useFlowControlLQDetermines the entire app navigation flow based on user state
useAutoFetchBalanceAuto-polling for balance with exponential backoff (max 5 retries)
useRemoteConfigFirebase remote config with local caching
useContactPermissionContact access for referral features
usePhysicalCardNudgeSmart nudge logic for physical card ordering
useFreeRaspUILayerRuntime security monitoring

Lessons Learned

1. Security is a spectrum, not a checkbox

No single security measure is sufficient. JailMonkey catches jailbroken devices, but FreeRASP catches Frida hooking. SSL pinning protects the transport, but ECDH protects the payload. Each layer catches what the others miss.

2. Native modules are worth the ceremony

The Objective-C bridge file for Swift modules is annoying boilerplate. The BouncyCastle/SpongyCastle classloader conflict took days to debug. But having encryption run in native code means keys never touch the JavaScript bridge unnecessarily — a meaningful security improvement for a fintech app.

3. CodePush is a superpower with responsibility

Being able to ship fixes in minutes instead of days is transformative. But every CodePush release needs source maps uploaded to Sentry, and you need to track which CodePush version maps to which native binary version. We encode this in our Sentry release string: com.streakcard@19.2.9+codepush@v69.

4. Animation libraries are not interchangeable

  • Reanimated for gesture-driven, physics-based, and layout animations
  • Lottie for complex vector animations designed in After Effects
  • Rive for interactive, state-machine-driven animations
  • Skia for GPU-accelerated custom drawing (squircles, gradients)
  • React Native Animated for simple, view-level transitions

Using the right tool for each case keeps performance predictable.

5. Squircles matter more than you think

Replacing borderRadius with proper Skia-drawn squircles made the entire app feel more polished. The difference is subtle — a standard rounded rectangle has discontinuous curvature at the tangent points, while a squircle has continuous curvature. Users notice, even if they can't articulate why.

6. Plan for the unhappy paths

Force update modals, maintenance screens, rooted device warnings, network error states, expired session handling, deep link queuing during PIN verification — the "sad paths" took as much engineering time as the happy paths. In fintech, graceful degradation isn't optional.

The Numbers

MetricCount
Custom native modules4 (CryptoReactNative, ECDHJavaAlgorithm, CryptoReactNativeIOS, InsecureNetworkCheck)
Security layers7 (JailMonkey, FreeRASP, SSL Pinning, Biometrics, Encrypted Storage, RSA, ProGuard+Hermes)
FreeRASP threat checks11 (9 common + 2 iOS-only)
Animation libraries5 (Reanimated, Lottie, Rive, Skia, RN Animated)
Files using Reanimated35+
Lottie animation files15+
Custom navigation transitions5 (Fade, Zoom, Right, Left, Bottom)
Shared element transition tags7+
Custom hooks10+
CodePush releases (Android)69
CodePush releases (iOS)46

Final Thoughts

Building StreakCard taught us that React Native is absolutely capable of powering a production fintech app — but you have to be willing to go beyond the JavaScript layer. The native modules, the SSL pinning configuration, the ProGuard rules, the Keychain clearing in AppDelegate.mm — these aren't glamorous, but they're what separates a prototype from a product that handles real money for real users.

The app ships with Hermes bytecode, ProGuard obfuscation, certificate pinning, ECDH encryption, biometric auth, runtime threat detection, and instant OTA updates. And thanks to Reanimated, Lottie, Rive, and Skia, it does all of this while feeling smooth and alive.

It's React Native. It's native. It's both.

Built with React Native. Secured with native code. Animated with care.