
In this tutorial, you'll learn how to create a game application called LukcyNumber from scratch. You'll create this app with React Native and implement secure player authentication and wallet management with Openfort. By the end of this tutorial, you'll have built a fully functional, cross-platform Web3 game app with a smooth native experience on both iOS and Android.
Here’s an overview of the application infrastructure:

Why Openfort?
When building a complete on-chain application, you need more than just authentication. You need a full-stack solution. Openfort provides a comprehensive infrastructure that consolidates everything in one place, from authentication to transactions and beyond, so you can focus on the user experience.
Building LuckyNumber: step-by-step
Now that you understand why using Openfort for your dApp is such a cool idea, it's time to build our LuckyNumber game from scratch.
Before we begin, this tutorial assumes that you're comfortable with building a simple React Native application independently. No prior knowledge of Openfort is required.
If you want to check the final code by yourself, here's the repository.
Requirements
Before following along, make sure you have the following setup:
- An Openfort account (sign up at dashboard.openfort.io)
- Expo environment setup (Node.js, Git, Watchman)
Note: As you build this project, be sure to check the comments in each code snippet for additional context on relevant code blocks.
1. Set up your Openfort Project
-
Start by creating a new project in the Openfort dashboard. Name your project "LuckyN", and then Create project:
-
Once created, your project will appear on the dashboard's list of projects. As seen below, select your project name:
-
Next, navigate to the API Keys tab in the left sidebar, and click on the
Create Shield keysbutton to open a modal prompting you to create shield keys. The shield API keys we are creating will allow our players to interact with wallet functionality directly in our app, instead of connecting to MetaMask or other external wallets: -
In the modal, select Create Shield keys* to generate shield keys:
-
Once the Shield key has been generated, copy the
Shield encryption keyand save it somewhere. This will be used in the next step: -
Finally, copy the
Publishable keysfrom both the Project keys and Shield key. This will be used in the next step:
2. Project Setup
To get started, we'll set up your React native app using Expo. This approach will allow us to create a cohesive experience across platforms.
Setting Up the React Native Project(IOS Version)
-
Create React Native App:
-
Open your terminal and run the following command to create a new React Native project using the latest Expo SDK:
_10npx create-expo-app@latest luckyN_10cd luckyN -
Run the development server, and if everything works correctly, you should see the Expo development server running. For this tutorial, I'll be using the iOS version of the app:
_10npm run ios

-
-
Install Dependencies:
- We'll use the Openfort React Native SDK to manage the wallet creation and authentication for our app, and its required peer dependencies to facilitate the foundational cryptographic and platform-specific features that make non-custodial embedded wallets possible in React Native. Install these dependencies by running:
_10# Install Openfort React Native SDK_10npm install @openfort/react-native_10_10# Install required dependencies_10yarn add expo-apple-authentication expo-application expo-crypto expo-secure-store react native-get-random-values -
Configure Metro for React Native Crypto Compatibility:
-
This Metro configuration fixes Jose library compatibility in React Native by forcing Metro to load the
browserversion instead of theNode.jsversion, since React Native's JavaScript environment doesn't support Node.js-specific APIs. Create a new file in your root folder by running the following command:_10touch metro.config.js -
In the newly created Metro config file, include the following:
_22// metro.config.js_22const { getDefaultConfig } = require("expo/metro-config");_22_22/** @type {import('expo/metro-config').MetroConfig} */_22const config = getDefaultConfig(__dirname);_22_22const resolveRequestWithPackageExports = (context, moduleName, platform) => {_22// Package exports in `jose` are incorrect, so we need to force the browser version_22if (moduleName === "jose") {_22const ctx = {_22...context,_22unstable_conditionNames: ["browser"],_22};_22return ctx.resolveRequest(ctx, moduleName, platform);_22}_22_22return context.resolveRequest(context, moduleName, platform);_22};_22_22config.resolver.resolveRequest = resolveRequestWithPackageExports;_22_22module.exports = config;
-
-
Set up Entry Point:
-
Create an
entrypoint.jsfile in your project root. This file is crucial for initializing the Openfort SDK and ensuring proper polyfill loading:_10// entrypoint.js_10_10// Import required polyfills first_10// IMPORTANT: These polyfills must be installed in this order_10_10import "react-native-get-random-values";_10// Then import the expo router_10_10import "expo-router/entry"; -
Finally, update your
package.jsonto use this entry point:_10{_10"main": "entrypoint.js",_10}
-
-
Adding your Openfort API Keys
-
To connect your app to Openfort's services, you need to add your API keys to the project configuration. Navigate to your projects
app.jsonfile and include your Openfort API keys:_11{_11"expo": {_11"name": "your-app-name",_11"slug": "your-app-slug",_11"extra": {_11"openfortPublishableKey": "your_publishable_key",_11"openfortShieldPublishableKey": "your_sheild_publishable_key",_11"openfortShieldEncryptionKey": "your_shield_encryption_key"_11}_11}_11}Security Best Practice: Always use environment variables for production apps. Never commit API keys directly to version control, especially when using public repositories.
-
Finally, extend your application plugin by including the following installed dependencies:
_20{_20"expo": {_20"name": "your-app-name",_20"slug": "your-app-slug",_20"plugins": [_20"expo-router",_20"expo-secure-store", // Secure Storage Plugin_20"expo-apple-authentication", // Social Logins Plugin_20[_20"expo-splash-screen",_20{_20"image": "./assets/images/splash-icon.png",_20"imageWidth": 200,_20"resizeMode": "contain",_20"backgroundColor": "#ffffff"_20}_20]_20],_20}_20}
-
-
Run the Application:
-
Start the Expo server and test the app on your device or simulator:
_10npm run ios
-
At this point, you now have a React Native app with Expo configured and ready for development. With these essential foundations in place, we can begin integrating Openfort's Web3 capabilities to add wallet functionality and user authentication to your application.
3. Adding Player Authentication with Openfort
Now that your project is set up, the next step is to integrate Openfort into your application for a complete Web3 experience.
To use the Openfort SDK, you need to wrap your entire application in the OpenfortProvider component, which provides access to all Openfort hooks and functionality throughout your app's component tree.
Setting Up Openfort in your Application
- Provider Setup:
-
Start by updating your
app/_layout.tsxto wrap your application in theOpenfortProvider:_39import {_39DarkTheme,_39DefaultTheme,_39ThemeProvider,_39} from "@react-navigation/native";_39import Constants from "expo-constants";_39import { useFonts } from "expo-font";_39import { Stack } from "expo-router";_39import { StatusBar } from "expo-status-bar";_39import "react-native-reanimated";_39_39import { useColorScheme } from "@/hooks/useColorScheme";_39import { OpenfortProvider } from "@openfort/react-native"; // Import the provider_39_39export default function RootLayout() {_39const colorScheme = useColorScheme();_39const [loaded] = useFonts({_39SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),_39});_39_39// Wait for fonts to load before rendering the app_39if (!loaded) {_39return null;_39}_39_39return (_39<OpenfortProvider_39publishableKey={Constants.expoConfig?.extra?.openfortPublishableKey} // Connect to your Openfort project_39>_39<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>_39<Stack>_39<Stack.Screen name="(tabs)" options={{ headerShown: false }} />_39<Stack.Screen name="+not-found" />_39</Stack>_39<StatusBar style="auto" />_39</ThemeProvider>_39</OpenfortProvider>_39);_39}
-
- Adding Wallet Configuration:
-
To enable wallet creation and management, extend the provider with wallet configuration properties:
_39import { OpenfortProvider, RecoveryMethod } from "@openfort/react-native"; // Add RecoveryMethod import_39_39export default function RootLayout() {_39const colorScheme = useColorScheme();_39const [loaded] = useFonts({_39SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),_39});_39_39if (!loaded) {_39return null;_39}_39_39return (_39<OpenfortProvider_39publishableKey={Constants.expoConfig?.extra?.openfortPublishableKey}_39walletConfig={{_39// Shield service for secure key management_39_39shieldPublishableKey:_39Constants.expoConfig?.extra?.openfortShieldPublishableKey,_39_39// Allow users to recover wallets with password_39recoveryMethod: RecoveryMethod.PASSWORD,_39_39// Encryption key for additional security_39shieldEncryptionKey:_39Constants.expoConfig?.extra?.openfortShieldEncryptionKey,_39}}_39>_39<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>_39<Stack>_39<Stack.Screen name="(tabs)" options={{ headerShown: false }} />_39<Stack.Screen name="+not-found" />_39</Stack>_39<StatusBar style="auto" />_39</ThemeProvider>_39</OpenfortProvider>_39);_39}
-
- Adding Blockchain Network Support:
-
Finally, specify which blockchain networks your app will support by adding the
supportedChainsconfiguration:_49export default function RootLayout() {_49const colorScheme = useColorScheme();_49const [loaded] = useFonts({_49SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),_49});_49_49if (!loaded) {_49return null;_49}_49_49return (_49<OpenfortProvider_49publishableKey={Constants.expoConfig?.extra?.openfortPublishableKey}_49walletConfig={{_49shieldPublishableKey:_49Constants.expoConfig?.extra?.openfortShieldPublishableKey,_49recoveryMethod: RecoveryMethod.PASSWORD,_49shieldEncryptionKey:_49Constants.expoConfig?.extra?.openfortShieldEncryptionKey,_49}}_49supportedChains={[_49{_49// Avalanche Fuji testnet configuration_49id: 43113, // Chain ID for Avalanche Fuji_49name: "Avalanche Fuji",_49nativeCurrency: {_49name: "Avalanche", // Display name_49symbol: "AVAX", // Currency symbol_49decimals: 18, // Standard ERC-20 decimals_49},_49rpcUrls: {_49default: {_49// RPC endpoint for connecting to the network_49http: ["https://api.avax-test.network/ext/bc/C/rpc"],_49},_49},_49},_49]}_49>_49<ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>_49<Stack>_49<Stack.Screen name="(tabs)" options={{ headerShown: false }} />_49<Stack.Screen name="+not-found" />_49</Stack>_49<StatusBar style="auto" />_49</ThemeProvider>_49</OpenfortProvider>_49);_49} -
With this setup complete, your app now has access to Openfort's infastructure, including wallet management capabilities. The provider makes these features available to any component in your app through Openfort's React hooks.
Note: We're using Avalanche Fuji testnet
(chain ID 43113)for development, which provides free AVAX tokens for testing. For production, you would add Avalanche mainnet (chain ID 43114).
-
Setting Up Protected Routes and Authentication
-
Create a Protected Area for Authenticated Players:
-
Create a new folder called
app/(tabs)/protected/. -
Move all existing files from
app/(tabs)/into the newprotected/folder. Your file structure should change as follows:_16# Before_16app/_16└── (tabs)/_16├── _layout.tsx_16├── explore.tsx_16└── index.tsx_16_16# After_16app/_16└── (tabs)/_16├── _layout.tsx # New: Main authentication router_16├── index.tsx # New: Login/authentication page_16└── protected/_16├── _layout.tsx # Moved: Protected area layout_16├── explore.tsx # Moved: Protected screen_16└── index.tsx # Moved: Protected home screen
-
-
Create Authentication Router:
-
Create a new layout file at
app/(tabs)/_layout.tsxthat handles routing between public and protected areas:_31import { useOpenfort } from "@openfort/react-native";_31import { Stack } from "expo-router";_31import { useEffect, useState } from "react";_31_31export default function AppLayout() {_31const { user } = useOpenfort(); // Get current user state_31const [isAuth, setIsAuth] = useState<boolean>(false);_31_31// Update authentication state when user changes_31useEffect(() => {_31if (user) {_31setIsAuth(true);_31} else {_31setIsAuth(false); // Reset to false when user logs out_31}_31}, [user]);_31_31return (_31<Stack screenOptions={{ headerShown: false }}>_31{/* Public routes - shown when user is NOT authenticated */}_31<Stack.Protected guard={!user}>_31<Stack.Screen name="index" /> {/* Login/Auth screen */}_31</Stack.Protected>_31_31{/* Protected routes - shown when user IS authenticated */}_31<Stack.Protected guard={isAuth}>_31<Stack.Screen name="protected" /> {/* Protected area */}_31</Stack.Protected>_31</Stack>_31);_31}
-
Handling Player Authentication
-
Create Authentication Page:
-
Create a dedicated styles file for consistent authentication UI across your app.
-
Create
constants/AuthStyles.ts:_164import { StyleSheet } from 'react-native';_164_164export const styles = StyleSheet.create({_164// Main container_164container: {_164flex: 1,_164backgroundColor: '#1a1a2e', // Dark theme background_164},_164_164// Content layout_164content: {_164flex: 1,_164justifyContent: "space-between",_164paddingHorizontal: 24,_164paddingVertical: 40,_164},_164_164// Header section_164header: {_164alignItems: "center",_164marginTop: 60,_164},_164logoContainer: {_164marginBottom: 24,_164},_164logo: {_164width: 80,_164height: 80,_164borderRadius: 40,_164backgroundColor: "rgba(255, 255, 255, 0.1)",_164justifyContent: "center",_164alignItems: "center",_164borderWidth: 2,_164borderColor: "rgba(255, 255, 255, 0.2)",_164},_164logoText: {_164fontSize: 40,_164},_164title: {_164fontSize: 24,_164color: '#ffffff',_164marginBottom: 8,_164fontWeight: "300",_164},_164appName: {_164fontSize: 32,_164fontWeight: "bold",_164color: '#ffffff',_164marginBottom: 12,_164textAlign: "center",_164},_164subtitle: {_164fontSize: 16,_164color: "rgba(255, 255, 255, 0.7)", // Semi-transparent white_164textAlign: "center",_164lineHeight: 22,_164},_164_164// Authentication section_164authSection: {_164flex: 1,_164justifyContent: "center",_164paddingVertical: 40,_164},_164_164// Guest login button_164guestButton: {_164borderRadius: 16,_164overflow: "hidden",_164marginBottom: 32,_164// Shadow effects for iOS_164shadowColor: "#ff6b6b",_164shadowOffset: { width: 0, height: 4 },_164shadowOpacity: 0.3,_164shadowRadius: 8,_164// Elevation for Android_164elevation: 8,_164},_164buttonGradient: {_164flexDirection: "row",_164alignItems: "center",_164justifyContent: "center",_164paddingVertical: 18,_164paddingHorizontal: 24,_164},_164guestButtonIcon: {_164fontSize: 20,_164marginRight: 12,_164},_164primaryButtonText: {_164color: '#ffffff',_164fontSize: 18,_164fontWeight: "600",_164letterSpacing: 0.5,_164},_164_164// Divider between auth methods_164dividerContainer: {_164flexDirection: "row",_164alignItems: "center",_164marginVertical: 24,_164},_164dividerLine: {_164flex: 1,_164height: 1,_164backgroundColor: "rgba(255, 255, 255, 0.2)", // Light divider line_164},_164dividerText: {_164color: "rgba(255, 255, 255, 0.6)",_164fontSize: 14,_164fontWeight: "500",_164marginHorizontal: 16,_164letterSpacing: 1,_164},_164_164// OAuth section_164oauthContainer: {_164gap: 16,_164},_164oauthButton: {_164backgroundColor: "rgba(255, 255, 255, 0.95)",_164borderRadius: 12,_164overflow: "hidden",_164// Shadow effects_164shadowColor: "#000",_164shadowOffset: { width: 0, height: 2 },_164shadowOpacity: 0.1,_164shadowRadius: 4,_164elevation: 4,_164},_164oauthButtonContent: {_164flexDirection: "row",_164alignItems: "center",_164justifyContent: "center",_164paddingVertical: 16,_164paddingHorizontal: 24,_164},_164googleIcon: {_164fontSize: 20,_164marginRight: 12,_164},_164oauthButtonText: {_164color: "#333333",_164fontSize: 16,_164fontWeight: "600",_164letterSpacing: 0.3,_164},_164_164// Error display_164errorContainer: {_164backgroundColor: "rgba(255, 59, 48, 0.1)",_164borderRadius: 8,_164padding: 12,_164marginTop: 16,_164borderWidth: 1,_164borderColor: "rgba(255, 59, 48, 0.3)",_164},_164errorText: {_164color: "#ff6b6b",_164fontSize: 14,_164textAlign: "center",_164fontWeight: "500",_164},_164}); -
Update your
app/(tabs)/index.tsxto create a comprehensive login screen with both guest and OAuth authentication options:_112import { OAuthProvider, useGuestAuth, useOAuth } from "@openfort/react-native";_112import { LinearGradient } from "expo-linear-gradient";_112import { styles } from '@/constants/AuthStyles';_112import {_112Alert,_112SafeAreaView,_112StatusBar,_112Text,_112TouchableOpacity,_112View,_112} from "react-native";_112_112export default function AuthScreen() {_112// Openfort authentication hooks_112const { signUpGuest } = useGuestAuth();_112const { initOAuth, error } = useOAuth();_112_112// Handle guest login with error handling_112const handleGuestLogin = async () => {_112try {_112await signUpGuest();_112// User will be automatically redirected to protected routes_112} catch (err) {_112console.error("Guest login error:", err);_112Alert.alert("Login Error", "Failed to login as guest. Please try again.");_112}_112};_112_112// Handle OAuth login with error handling_112const handleOAuthLogin = async (provider: string) => {_112try {_112await initOAuth({ provider: provider as OAuthProvider });_112// User will be automatically redirected to protected routes_112} catch (err) {_112console.error(`OAuth login error with ${provider}:`, err);_112Alert.alert(_112"Login Error",_112`Failed to login with ${provider}. Please try again.`_112);_112}_112};_112_112return (_112<SafeAreaView style={styles.container}>_112{/* Status bar configuration for dark theme */}_112<StatusBar barStyle="light-content" backgroundColor="#1a1a2e" />_112_112<View style={styles.content}>_112{/* App branding header */}_112<View style={styles.header}>_112<View style={styles.logoContainer}>_112<View style={styles.logo}>_112<Text style={styles.logoText}>🎲</Text>_112</View>_112</View>_112_112<Text style={styles.title}>Welcome to</Text>_112<Text style={styles.appName}>Openfort LuckyN</Text>_112<Text style={styles.subtitle}>_112Your Web3 gaming experience starts here_112</Text>_112</View>_112_112{/* Authentication options */}_112<View style={styles.authSection}>_112{/* Guest login - fastest way to get started */}_112<TouchableOpacity_112style={styles.guestButton}_112onPress={handleGuestLogin}_112activeOpacity={0.8}_112>_112<LinearGradient_112colors={["#ff6b6b", "#ee5a52"]} // Red gradient_112style={styles.buttonGradient}_112>_112<Text style={styles.guestButtonIcon}>👤</Text>_112<Text style={styles.primaryButtonText}>Continue as Guest</Text>_112</LinearGradient>_112</TouchableOpacity>_112_112{/* Divider between auth methods */}_112<View style={styles.dividerContainer}>_112<View style={styles.dividerLine} />_112<Text style={styles.dividerText}>OR</Text>_112<View style={styles.dividerLine} />_112</View>_112_112{/* OAuth login options */}_112<View style={styles.oauthContainer}>_112<TouchableOpacity_112style={styles.oauthButton}_112onPress={() => handleOAuthLogin("google")}_112activeOpacity={0.8}_112>_112<View style={styles.oauthButtonContent}>_112<Text style={styles.googleIcon}>📧</Text>_112<Text style={styles.oauthButtonText}>Continue with Google</Text>_112</View>_112</TouchableOpacity>_112</View>_112_112{/* Error display - shows OAuth errors */}_112{error && (_112<View style={styles.errorContainer}>_112<Text style={styles.errorText}>⚠️ {error.message}</Text>_112</View>_112)}_112</View>_112</View>_112</SafeAreaView>_112);_112} -
With your
app/(tabs)/index.tsxupdated, this will ensure that the first screen a player sees when they access the platform is the login page:
-
-
Configure Google OAuth Setup:
-
To enable Google OAuth authentication, you need to configure credentials in both Google Cloud Console and your Openfort dashboard.
-
Visit the Google Cloud Console to configure your OAuth credentials. When creating a new OAuth client ID, choose Android or iOS depending on the mobile operating system your app is built for.
-
Navigate to the Configuration tab in the left sidebar in your Openfort Dashboard, and expand the Google authentication section:
-
Toggle the Enable Sign in with Google button:
-
Paste your application
Client IDsandClient Secretfrom your Google Cloud Project, and save your configuration: -
Authentication should be fully configured at this point. Start the development server by running the following command in your terminal:
_10npm run ios -
Your application should look like this after executing the above commmand:

-
4. Implementing the Game Screen
Our goal in this section is to create the Lucky Number game and implementing wallet-gated access. Only authenticated users with active wallets can play the game, demonstrating how to build engaging Web3 gaming experiences.

-
Create the Game Component:
-
Create
constants/GameStyles.tsand paste the game style:_101import { StyleSheet, Dimensions } from 'react-native';_101_101const { width } = Dimensions.get('window');_101_101export const styles = StyleSheet.create({_101// Main container_101container: {_101alignItems: "center",_101padding: 20,_101backgroundColor: '#f8f9fa', // Light background_101},_101_101// Game title_101title: {_101fontSize: 24,_101fontWeight: "bold",_101color: "#333333", // Dark text for readability_101marginBottom: 20,_101textAlign: "center",_101},_101_101// Countdown timer_101countdownContainer: {_101width: 60,_101height: 60,_101borderRadius: 30,_101backgroundColor: "#ff6b6b", // Red background for urgency_101justifyContent: "center",_101alignItems: "center",_101marginBottom: 20,_101},_101countdown: {_101fontSize: 28,_101fontWeight: "bold",_101color: "#ffffff",_101},_101_101// Numbers grid layout_101numbersContainer: {_101flexDirection: "row",_101flexWrap: "wrap",_101justifyContent: "center",_101gap: 10,_101marginBottom: 30,_101},_101numberCard: {_101width: (width - 80) / 3, // Responsive width for 3 cards per row_101height: 80,_101backgroundColor: "#f0f0f0",_101borderRadius: 8,_101justifyContent: "center",_101alignItems: "center",_101borderWidth: 2,_101borderColor: "#ddd",_101},_101// Selected target number styling_101targetCard: {_101backgroundColor: "#3CB371", // Green for selected target_101borderColor: "#7CFC00",_101},_101// User's guess selection_101selectedCard: {_101backgroundColor: "#ff6b6b", // Red for selected guess_101borderColor: "#ff6b6b",_101},_101numberText: {_101fontSize: 32,_101fontWeight: "bold",_101color: "#333333",_101},_101_101// Game instructions_101instructionContainer: {_101backgroundColor: "rgba(0, 0, 0, 0.05)", // Subtle background_101padding: 15,_101borderRadius: 8,_101marginHorizontal: 20,_101},_101instructionText: {_101fontSize: 16,_101color: "#333333",_101textAlign: "center",_101},_101_101// Reset/New Game button_101resetButton: {_101marginTop: 20,_101backgroundColor: "rgba(46, 213, 115, 0.2)",_101borderRadius: 12,_101paddingVertical: 12,_101paddingHorizontal: 24,_101borderWidth: 1,_101borderColor: "#2ed573",_101},_101resetButtonText: {_101color: "#2ed573",_101fontSize: 16,_101fontWeight: "600",_101textAlign: "center",_101},_101});-
Create
components/NumberPreview.tsxand paste the following code:_213import React, { useEffect, useState } from "react";_213import {_213Text,_213TouchableOpacity,_213View,_213} from "react-native";_213import { styles } from '../constants/GameStyles';_213_213interface NumberPreviewProps {_213numbers: number[];_213onNumberSelect: (selectedNumber: number, isCorrect: boolean) => void;_213onGameReset?: () => void;_213}_213_213export const NumberPreview: React.FC<NumberPreviewProps> = ({_213numbers,_213onNumberSelect,_213onGameReset,_213}) => {_213// Game state management_213const [countdown, setCountdown] = useState(5);_213const [gamePhase, setGamePhase] = useState<_213"select" | "ready" | "shuffle" | "guess" | "failed"_213>("select");_213const [selectedTargetNumber, setSelectedTargetNumber] = useState<number | null>(null);_213const [shuffledNumbers, setShuffledNumbers] = useState(numbers);_213const [correctPosition, setCorrectPosition] = useState(0);_213const [selectedCard, setSelectedCard] = useState<number | null> (null);_213_213// Countdown timer effect for ready phase_213useEffect(() => {_213if (gamePhase === "ready" && countdown > 0) {_213const timer = setTimeout(() => {_213setCountdown(countdown - 1);_213}, 1000);_213return () => clearTimeout(timer);_213} else if (countdown === 0 && gamePhase === "ready") {_213startShufflePhase();_213}_213}, [countdown, gamePhase]);_213_213// Handle player selecting their target number_213const handleTargetNumberSelect = (number: number) => {_213if (gamePhase !== "select") return;_213_213setSelectedTargetNumber(number);_213setGamePhase("ready");_213setCountdown(5); // Start 5-second countdown_213};_213_213// Begin the shuffle phase_213const startShufflePhase = () => {_213setGamePhase("shuffle");_213_213// Brief delay for visual feedback before shuffle_213setTimeout(() => {_213shuffleNumbers();_213}, 1000);_213};_213_213// Shuffle the numbers and track target position_213const shuffleNumbers = () => {_213// Fisher-Yates shuffle algorithm_213const shuffled = [...numbers];_213for (let i = shuffled.length - 1; i > 0; i--) {_213const j = Math.floor(Math.random() * (i + 1));_213[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];_213}_213_213// Find where the target number ended up_213const newTargetIndex = shuffled.indexOf(selectedTargetNumber!);_213_213setShuffledNumbers(shuffled);_213setCorrectPosition(newTargetIndex);_213setGamePhase("guess");_213};_213_213// Handle player's guess_213const handleCardPress = (index: number) => {_213if (gamePhase !== "guess" || selectedCard !== null) return;_213_213setSelectedCard(index);_213const isCorrect = index === correctPosition;_213_213if (!isCorrect) {_213setGamePhase("failed");_213}_213_213// Delay feedback to show selection_213setTimeout(() => {_213onNumberSelect(selectedTargetNumber!, isCorrect);_213}, 500);_213};_213_213// Reset game to initial state_213const resetGame = () => {_213setCountdown(5);_213setGamePhase("select");_213setSelectedTargetNumber(null);_213setSelectedCard(null);_213setShuffledNumbers(numbers);_213setCorrectPosition(0);_213_213// Call optional reset callback_213if (onGameReset) {_213onGameReset();_213}_213};_213_213// Dynamic title based on game phase_213const getPhaseTitle = () => {_213switch (gamePhase) {_213case "select":_213return "Choose Your Lucky Number!";_213case "ready":_213return `Get Ready! Your number: ${selectedTargetNumber}`;_213case "shuffle":_213return "Shuffling...";_213case "guess":_213return `Find: ${selectedTargetNumber}`;_213case "failed":_213return "Game Over!";_213default:_213return "";_213}_213};_213_213// Get current number array based on game phase_213const getCurrentNumbers = () => {_213if (gamePhase === "select" || gamePhase === "ready") {_213return numbers; // Show original positions_213}_213return shuffledNumbers; // Show shuffled positions_213};_213_213// Determine whether to show number or hide it_213const showNumber = (number: number, index: number) => {_213// Always show during select, ready, and failed phases_213if (gamePhase === "select" || gamePhase === "ready" || gamePhase === "failed") {_213return true;_213}_213// During guess phase, only show if card is selected_213return selectedCard === index;_213};_213_213return (_213<View style={styles.container}>_213{/* Dynamic game title */}_213<Text style={styles.title}>{getPhaseTitle()}</Text>_213_213{/* Countdown timer (only during ready phase) */}_213{gamePhase === "ready" && (_213<View style={styles.countdownContainer}>_213<Text style={styles.countdown}>{countdown}</Text>_213</View>_213)}_213_213{/* Numbers grid */}_213<View style={styles.numbersContainer}>_213{getCurrentNumbers().map((number, index) => {_213const isTargetNumber =_213selectedTargetNumber === number && gamePhase !== "guess";_213const isSelected = selectedCard === index;_213_213return (_213<TouchableOpacity_213key={`${gamePhase}-${index}`}_213style={[_213styles.numberCard,_213isTargetNumber && styles.targetCard, // Highlight selected target_213isSelected && styles.selectedCard, // Highlight player's guess_213]}_213onPress={() => {_213if (gamePhase === "select") {_213handleTargetNumberSelect(number);_213} else if (gamePhase === "guess") {_213handleCardPress(index);_213}_213}}_213disabled={_213gamePhase === "ready" ||_213gamePhase === "shuffle" ||_213gamePhase === "failed"_213}_213>_213<Text style={styles.numberText}>_213{showNumber(number, index) ? number : "?"}_213</Text>_213</TouchableOpacity>_213);_213})}_213</View>_213_213{/* Dynamic instructions */}_213<View style={styles.instructionContainer}>_213<Text style={styles.instructionText}>_213{gamePhase === "select" && "Tap a number to begin!"}_213{gamePhase === "ready" && `Shuffling in ${countdown} seconds...`}_213{gamePhase === "shuffle" && "Shuffling numbers..."}_213{gamePhase === "guess" && "Tap where you think your number is!"}_213{gamePhase === "failed" && "Better luck next time!"}_213</Text>_213</View>_213_213{/* Reset button (show after game ends) */}_213{(gamePhase === "guess" || gamePhase === "failed") && (_213<TouchableOpacity style={styles.resetButton} onPress= {resetGame}>_213<Text style={styles.resetButtonText}>New Game</Text>_213</TouchableOpacity>_213)}_213</View>_213);_213};
-
-
-
Implement Wallet-Gated Access
-
Update your protected home screen to integrate the game with wallet requirements. Replace `app/(tabs)/protected/index.tsx with what's below:
_178import { useOpenfort, useWallets } from "@openfort/react-native";_178import { useState } from "react";_178import { Alert, StyleSheet, Text, View } from "react-native";_178import { SafeAreaView } from "react-native-safe-area-context";_178import { NumberPreview } from "../../../components/ NumberPreview"; // Updated import_178_178export default function HomeScreen() {_178const { user } = useOpenfort(); // Get authenticated user_178const { activeWallet } = useWallets(); // Get active wallet state_178const [gameNumbers] = useState([7, 23, 91, 45, 8]); // Static game numbers_178_178// Handle game result with user feedback_178const handleNumberSelect = (selectedNumber: number, isCorrect: boolean) => {_178if (isCorrect) {_178Alert.alert(_178"🎉 Congratulations!",_178`You found ${selectedNumber} in the right position!`_178);_178} else {_178Alert.alert(_178"❌ Close!",_178`${selectedNumber} was in a different position. Try again!`_178);_178}_178};_178_178_178return (_178<SafeAreaView style={styles.container}>_178<View style={styles.content}>_178{/* Header section */}_178<View style={styles.header}>_178<Text style={styles.title}>Lucky Number Game</Text>_178<Text style={styles.subtitle}>Welcome back!</Text>_178</View>_178_178{/* User info display */}_178{user && (_178<View style={styles.userInfo}>_178<Text style={styles.userLabel}>Player ID:</Text>_178<Text style={styles.userId}>{user.id}</Text>_178</View>_178)}_178_178{/* Main game area */}_178<View style={styles.gameContainer}>_178{activeWallet ? (_178// Show game when wallet is connected_178<NumberPreview_178numbers={gameNumbers}_178onNumberSelect={handleNumberSelect}_178onGameReset={handleGameReset}_178/>_178) : (_178// Show wallet prompt when no wallet connected_178<View style={styles.noWalletContainer}>_178<View style={styles.noWalletIconContainer}>_178<Text style={styles.noWalletIcon}>🎯</Text>_178</View>_178<Text style={styles.noWalletTitle}>Wallet Required</Text>_178<Text style={styles.noWalletMessage}>_178Connect your wallet to start playing the Lucky Number game!_178</Text>_178<Text style={styles.noWalletHint}>_178💡 Go to Settings to connect your wallet_178</Text>_178</View>_178)}_178</View>_178</View>_178</SafeAreaView>_178);_178}_178_178const styles = StyleSheet.create({_178container: {_178flex: 1,_178backgroundColor: '#f8f9fa',_178},_178content: {_178flex: 1,_178paddingHorizontal: 16,_178},_178_178// Header styles_178header: {_178paddingVertical: 20,_178alignItems: 'center',_178},_178title: {_178fontSize: 28,_178fontWeight: "bold",_178color: '#333',_178marginBottom: 4,_178},_178subtitle: {_178fontSize: 16,_178color: '#666',_178},_178_178// User info display_178userInfo: {_178backgroundColor: 'rgba(0, 0, 0, 0.05)',_178borderRadius: 8,_178padding: 12,_178marginBottom: 20,_178flexDirection: 'row',_178alignItems: 'center',_178},_178userLabel: {_178fontSize: 14,_178fontWeight: '600',_178color: '#666',_178marginRight: 8,_178},_178userId: {_178fontSize: 14,_178fontFamily: 'monospace', // Monospace for ID display_178color: '#333',_178flex: 1,_178},_178_178// Game container_178gameContainer: {_178flex: 1,_178justifyContent: 'center',_178},_178_178// No wallet state styles_178noWalletContainer: {_178backgroundColor: 'rgba(255, 255, 255, 0.9)',_178borderRadius: 16,_178padding: 32,_178alignItems: 'center',_178marginHorizontal: 16,_178borderWidth: 1,_178borderColor: 'rgba(0, 0, 0, 0.1)',_178// Shadow for iOS_178shadowColor: '#000',_178shadowOffset: { width: 0, height: 2 },_178shadowOpacity: 0.1,_178shadowRadius: 8,_178// Elevation for Android_178elevation: 4,_178},_178noWalletIconContainer: {_178width: 80,_178height: 80,_178borderRadius: 40,_178backgroundColor: 'rgba(0, 0, 0, 0.05)',_178justifyContent: 'center',_178alignItems: 'center',_178marginBottom: 20,_178},_178noWalletIcon: {_178fontSize: 40,_178},_178noWalletTitle: {_178fontSize: 24,_178fontWeight: 'bold',_178color: '#333',_178marginBottom: 12,_178textAlign: 'center',_178},_178noWalletMessage: {_178fontSize: 16,_178color: '#666',_178textAlign: 'center',_178lineHeight: 24,_178marginBottom: 16,_178},_178noWalletHint: {_178fontSize: 14,_178color: '#888',_178textAlign: 'center',_178fontStyle: 'italic',_178},_178}); -
Now access the application in your simulator again,and you'd see this when a player logs in:
-

-
With the Openfort
useOpenforthook, we can now retrieve our player's information. -
Visit the Openfort dashboard to see the list of players that joined the game:
4. Implementing the Settings Screen
The last thing you'll build in this demo is the settings screen that provides essential player management functionality, allowing players to create and manage wallets, switch between different wallets, and sign out of the application. Two helper functions form the Openfort SDK will be used: the useOpenfort hook to get currently logged in user and useWallets hook to access the function required to manage wallets.
Setting Up the Settings Screen
-
Create the
app/(tabs)/protected/settings.tsxfile and add the following code:_348import { useOpenfort, useWallets } from "@openfort/react-native";_348import { useRouter } from "expo-router";_348import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native";_348import { SafeAreaView } from "react-native-safe-area-context";_348_348export default function SettingsScreen() {_348const router = useRouter();_348const { logout } = useOpenfort(); // Get logout function from Openfort_348_348// Wallet management hooks with error handling_348const { wallets, setActiveWallet, createWallet, activeWallet, isCreating } = useWallets({_348onError: (error) => {_348console.error("Wallet Error:", error);_348Alert.alert("Wallet Error", error.message || "An error occurred with wallet operations");_348},_348});_348_348// Temporary password for demo - in production, use secure password management_348const recoveryPassword = "test123";_348_348// Handle user logout with navigation_348const handleLogout = () => {_348Alert.alert(_348"Confirm Logout",_348"Are you sure you want to sign out?",_348[_348{ text: "Cancel", style: "cancel" },_348{_348text: "Sign Out",_348style: "destructive",_348onPress: () => {_348logout();_348router.replace("/(tabs)"); // Navigate back to auth screen_348},_348},_348]_348);_348};_348_348// Create new wallet with error handling_348const handleCreateWallet = async () => {_348try {_348await createWallet({_348recoveryPassword: recoveryPassword,_348});_348Alert.alert("Success", "New wallet created successfully!");_348} catch (error) {_348console.error("Wallet creation failed:", error);_348Alert.alert("Error", "Failed to create wallet. Please try again.");_348}_348};_348_348// Set active wallet with comprehensive error handling_348const handleSetActiveWallet = async (walletAddress: string) => {_348try {_348await setActiveWallet({_348address: walletAddress,_348chainId: 43113, // Avalanche Fuji testnet_348recoveryPassword: recoveryPassword,_348onSuccess: () => {_348Alert.alert("Success", `Wallet ${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)} is now active`);_348},_348onError: (error) => {_348Alert.alert("Error", `Failed to set active wallet: ${error.message}`);_348},_348});_348} catch (error) {_348console.error("Set active wallet failed:", error);_348Alert.alert("Error", "Failed to activate wallet. Please try again.");_348}_348};_348_348return (_348<SafeAreaView style={styles.container}>_348<View style={styles.content}>_348{/* Header */}_348<View style={styles.header}>_348<Text style={styles.title}>Settings</Text>_348<Text style={styles.subtitle}>Manage your wallets and account</Text>_348</View>_348_348{/* Wallet Management Section */}_348<View style={styles.walletContainer}>_348<Text style={styles.walletSectionTitle}>_348{wallets.length > 0 ? "Your Wallets" : "No Wallets Available"}_348</Text>_348_348{wallets.length > 0 ? (_348<Text style={styles.walletDescription}>_348Tap a wallet to make it active. Active wallets can be used for transactions._348</Text>_348) : (_348<Text style={styles.walletDescription}>_348Create your first wallet to start playing games and making transactions._348</Text>_348)}_348_348{/* Wallet List */}_348<View style={styles.walletList}>_348{wallets.map((wallet, index) => {_348const isActive = activeWallet?.address === wallet.address;_348const displayAddress = `${wallet.address.slice(0, 8)}...${wallet.address.slice(-6)}`;_348_348return (_348<View key={wallet.address + index} style={styles.walletItem}>_348<TouchableOpacity_348style={[_348styles.walletButton,_348isActive && styles.activeWalletButton,_348]}_348onPress={() => handleSetActiveWallet(wallet.address)}_348disabled={isActive || wallet.isConnecting}_348>_348<View style={styles.walletButtonContent}>_348<Text style={[_348styles.walletAddress,_348isActive && styles.activeWalletText,_348]}>_348{displayAddress}_348</Text>_348{isActive && (_348<View style={styles.activeBadge}>_348<Text style={styles.activeBadgeText}>ACTIVE</Text>_348</View>_348)}_348</View>_348</TouchableOpacity>_348_348{/* Connection status */}_348{wallet.isConnecting && (_348<Text style={styles.connectingText}>Connecting..</Text>_348)}_348</View>_348);_348})}_348</View>_348_348{/* Create Wallet Button */}_348<TouchableOpacity_348style={[_348styles.createWalletButton,_348isCreating && styles.disabledButton,_348]}_348onPress={handleCreateWallet}_348disabled={isCreating}_348>_348<Text style={styles.createWalletButtonText}>_348{isCreating ? "Creating Wallet..." : "+ Create New Wallet"}_348</Text>_348</TouchableOpacity>_348</View>_348_348{/* Additional Settings */}_348<View style={styles.additionalSettings}>_348<Text style={styles.sectionTitle}>Account</Text>_348_348<TouchableOpacity style={styles.settingItem}>_348<Text style={styles.settingItemText}>Backup & Recovery</Text>_348<Text style={styles.settingItemSubtext}>Manage wallet backup</Text>_348</TouchableOpacity>_348_348<TouchableOpacity style={styles.settingItem}>_348<Text style={styles.settingItemText}>Security</Text>_348<Text style={styles.settingItemSubtext}>Password and security settings</ Text>_348</TouchableOpacity>_348</View>_348</View>_348_348{/* Logout Section */}_348<View style={styles.logoutContainer}>_348<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>_348<Text style={styles.logoutText}>Sign Out</Text>_348</TouchableOpacity>_348</View>_348</SafeAreaView>_348);_348}_348_348const styles = StyleSheet.create({_348container: {_348flex: 1,_348backgroundColor: '#f8f9fa',_348},_348content: {_348flex: 1,_348paddingHorizontal: 16,_348},_348_348// Header_348header: {_348paddingVertical: 24,_348},_348title: {_348fontSize: 28,_348fontWeight: "bold",_348color: '#333',_348marginBottom: 4,_348},_348subtitle: {_348fontSize: 16,_348color: '#666',_348},_348_348// Wallet Section_348walletContainer: {_348marginBottom: 32,_348},_348walletSectionTitle: {_348fontSize: 20,_348fontWeight: "bold",_348color: '#333',_348marginBottom: 8,_348},_348walletDescription: {_348fontSize: 14,_348color: '#666',_348marginBottom: 20,_348lineHeight: 20,_348},_348walletList: {_348marginBottom: 20,_348},_348walletItem: {_348marginBottom: 12,_348},_348walletButton: {_348backgroundColor: '#fff',_348borderRadius: 12,_348padding: 16,_348borderWidth: 1,_348borderColor: '#e0e0e0',_348shadowColor: '#000',_348shadowOffset: { width: 0, height: 1 },_348shadowOpacity: 0.1,_348shadowRadius: 2,_348elevation: 2,_348},_348activeWalletButton: {_348borderColor: '#28a745',_348backgroundColor: '#f8fff9',_348},_348walletButtonContent: {_348flexDirection: 'row',_348alignItems: 'center',_348justifyContent: 'space-between',_348},_348walletAddress: {_348fontSize: 16,_348fontFamily: 'monospace',_348color: '#333',_348fontWeight: '500',_348},_348activeWalletText: {_348color: '#28a745',_348},_348activeBadge: {_348backgroundColor: '#28a745',_348borderRadius: 12,_348paddingHorizontal: 8,_348paddingVertical: 4,_348},_348activeBadgeText: {_348color: '#fff',_348fontSize: 10,_348fontWeight: 'bold',_348},_348connectingText: {_348color: '#666',_348fontSize: 12,_348fontStyle: "italic",_348marginTop: 4,_348textAlign: 'center',_348},_348_348// Create Wallet Button_348createWalletButton: {_348backgroundColor: '#007AFF',_348borderRadius: 12,_348paddingVertical: 16,_348paddingHorizontal: 24,_348alignItems: 'center',_348shadowColor: '#000',_348shadowOffset: { width: 0, height: 2 },_348shadowOpacity: 0.1,_348shadowRadius: 4,_348elevation: 3,_348},_348disabledButton: {_348backgroundColor: '#ccc',_348opacity: 0.6,_348},_348createWalletButtonText: {_348color: '#fff',_348fontSize: 16,_348fontWeight: '600',_348},_348_348// Additional Settings_348additionalSettings: {_348flex: 1,_348},_348sectionTitle: {_348fontSize: 18,_348fontWeight: 'bold',_348color: '#333',_348marginBottom: 16,_348},_348settingItem: {_348backgroundColor: '#fff',_348borderRadius: 12,_348padding: 16,_348marginBottom: 8,_348borderWidth: 1,_348borderColor: '#e0e0e0',_348},_348settingItemText: {_348fontSize: 16,_348fontWeight: '500',_348color: '#333',_348marginBottom: 4,_348},_348settingItemSubtext: {_348fontSize: 14,_348color: '#666',_348},_348_348// Logout_348logoutContainer: {_348paddingVertical: 24,_348paddingHorizontal: 16,_348},_348logoutButton: {_348backgroundColor: '#ff3b30',_348borderRadius: 12,_348paddingVertical: 16,_348alignItems: 'center',_348shadowColor: '#000',_348shadowOffset: { width: 0, height: 2 },_348shadowOpacity: 0.1,_348shadowRadius: 4,_348elevation: 3,_348},_348logoutText: {_348color: '#fff',_348fontSize: 16,_348fontWeight: '600',_348},_348}); -
Configure Tab Navigation:
- Update the protected area's tab layout to include the settings screen. Update
app/(tabs)/protected/_layout.tsx:
_53import { Tabs } from "expo-router";_53import React from "react";_53import { Platform } from "react-native";_53_53import { HapticTab } from "@/components/HapticTab";_53import { IconSymbol } from "@/components/ui/IconSymbol";_53import TabBarBackground from "@/components/ui/TabBarBackground";_53import { Colors } from "@/constants/Colors";_53import { useColorScheme } from "@/hooks/useColorScheme";_53_53export default function TabLayout() {_53const colorScheme = useColorScheme();_53_53return (_53<Tabs_53screenOptions={{_53tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,_53headerShown: false,_53tabBarButton: HapticTab,_53tabBarBackground: TabBarBackground,_53tabBarStyle: Platform.select({_53ios: {_53// Floating tab bar on iOS_53position: "absolute",_53},_53default: {},_53}),_53}}_53>_53{/* Home Tab */}_53<Tabs.Screen_53name="index"_53options={{_53title: "Home",_53tabBarIcon: ({ color }) => (_53<IconSymbol size={28} name="house.fill" color={color} />_53),_53}}_53/>_53_53{/* Settings Tab */}_53<Tabs.Screen_53name="settings"_53options={{_53title: "Settings",_53tabBarIcon: ({ color }) => (_53<IconSymbol size={28} name="gear" color={color} />_53),_53}}_53/>_53</Tabs>_53);_53} - Update the protected area's tab layout to include the settings screen. Update
-
Add the gear icon mapping for consistent cross-platform display. Update
components/ui/IconSymbol.tsx:_51// Fallback for using MaterialIcons on Android and web._51import MaterialIcons from "@expo/vector-icons/MaterialIcons";_51import { SymbolViewProps, SymbolWeight } from "expo-symbols";_51import { ComponentProps } from "react";_51import { OpaqueColorValue, type StyleProp, type TextStyle } from "react-native";_51_51type IconMapping = Record<_51SymbolViewProps["name"],_51ComponentProps<typeof MaterialIcons>["name"]_51>;_51type IconSymbolName = keyof typeof MAPPING;_51_51/**_51* SF Symbols to Material Icons mappings_51* - SF Symbols: https://developer.apple.com/sf-symbols/_51* - Material Icons: https://icons.expo.fyi_51*/_51const MAPPING = {_51"house.fill": "home",_51"paperplane.fill": "send",_51"chevron.left.forwardslash.chevron.right": "code",_51"chevron.right": "chevron-right",_51gear: "settings", // Added mapping for settings icon_51} as IconMapping;_51_51/**_51* Cross-platform icon component_51* Uses SF Symbols on iOS and Material Icons on Android/web_51*/_51export function IconSymbol({_51name,_51size = 24,_51color,_51style,_51weight,_51}: {_51name: IconSymbolName;_51size?: number;_51color: string | OpaqueColorValue;_51style?: StyleProp<TextStyle>;_51weight?: SymbolWeight;_51}) {_51return (_51<MaterialIcons_51color={color}_51size={size}_51name={MAPPING[name]}_51style={style}_51/>_51);_51} -
Delete the
app/(tabs)/protected/explore.tsxscreen since it's no longer used:_10rm app/(tabs)/protected/explore.tsx -
Navigate to the settings page to add new wallets or signout as seen below:

- Finally, head back home and the homescreen should now be displaying the lukcy number game, as seen below:
5. Wallet Overview
To view wallet details for your application users:
-
Open the Openfort Dashboard and navigate to the Users tab in the left sidebar.
-
Select a Account from the list to view their profile:
-
Expand the "Embedded Wallets" Section to view wallet details:
The wallet information displays the blockchain network configuration that matches your OpenfortProvider setup in app/_layout.tsx. This confirms that wallets are created on the correct network (Avalanche Fuji testnet with chain ID 43113).
Multi-Chain Support
The supportedChains configuration in your provider enables:
- Support for multiple blockchain networks
- User ability to create wallets on different chains
- Network-specific wallet management
Note: Currently, your application supports only Avalanche Fuji testnet. To add additional networks, update the
supportedChainsarray in your provider configuration.
6. Conclusion
You’ve successfully built a Web3-powered React Native app using Expo and Openfort’s embedded wallet infrastructure. Along the way, you learned how to set up secure authentication, manage wallets with Openfort, and even created a simple game where players guess the position of their lucky number.
Combining React Native with Openfort, you now have a strong foundation for building secure, scalable mobile apps that can grow with your needs. In the next article, we’ll dive deeper into signing messages, minting NFTs, and handling transactions with the Openfort React Native SDK. Let’s unlock even more powerful features for your players!