Using Embedded Wallets with React Native app

29 min read

Using Embedded Wallets with React Native app

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:

infra.png

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:

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

  1. Start by creating a new project in the Openfort dashboard. Name your project "LuckyN", and then Create project:

  2. Once created, your project will appear on the dashboard's list of projects. As seen below, select your project name:

  3. Next, navigate to the API Keys tab in the left sidebar, and click on the Create Shield keys button 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:

  4. In the modal, select Create Shield keys* to generate shield keys:

  5. Once the Shield key has been generated, copy the Shield encryption key and save it somewhere. This will be used in the next step:

  6. Finally, copy the Publishable keys from 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)

  1. Create React Native App:

    • Open your terminal and run the following command to create a new React Native project using the latest Expo SDK:


      _10
      npx create-expo-app@latest luckyN
      _10
      cd 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:


      _10
      npm run ios

    simulator.png

  2. 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
    _10
    npm install @openfort/react-native
    _10
    _10
    # Install required dependencies
    _10
    yarn add expo-apple-authentication expo-application expo-crypto expo-secure-store react native-get-random-values

  3. Configure Metro for React Native Crypto Compatibility:

    • This Metro configuration fixes Jose library compatibility in React Native by forcing Metro to load the browser version instead of the Node.js version, 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:


      _10
      touch metro.config.js

    • In the newly created Metro config file, include the following:


      _22
      // metro.config.js
      _22
      const { getDefaultConfig } = require("expo/metro-config");
      _22
      _22
      /** @type {import('expo/metro-config').MetroConfig} */
      _22
      const config = getDefaultConfig(__dirname);
      _22
      _22
      const resolveRequestWithPackageExports = (context, moduleName, platform) => {
      _22
      // Package exports in `jose` are incorrect, so we need to force the browser version
      _22
      if (moduleName === "jose") {
      _22
      const ctx = {
      _22
      ...context,
      _22
      unstable_conditionNames: ["browser"],
      _22
      };
      _22
      return ctx.resolveRequest(ctx, moduleName, platform);
      _22
      }
      _22
      _22
      return context.resolveRequest(context, moduleName, platform);
      _22
      };
      _22
      _22
      config.resolver.resolveRequest = resolveRequestWithPackageExports;
      _22
      _22
      module.exports = config;

  4. Set up Entry Point:

    • Create an entrypoint.js file 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
      _10
      import "react-native-get-random-values";
      _10
      // Then import the expo router
      _10
      _10
      import "expo-router/entry";

    • Finally, update your package.json to use this entry point:


      _10
      {
      _10
      "main": "entrypoint.js",
      _10
      }

  5. 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.json file 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
      }

  6. Run the Application:

    • Start the Expo server and test the app on your device or simulator:


      _10
      npm 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

  1. Provider Setup:
    • Start by updating your app/_layout.tsx to wrap your application in the OpenfortProvider:


      _39
      import {
      _39
      DarkTheme,
      _39
      DefaultTheme,
      _39
      ThemeProvider,
      _39
      } from "@react-navigation/native";
      _39
      import Constants from "expo-constants";
      _39
      import { useFonts } from "expo-font";
      _39
      import { Stack } from "expo-router";
      _39
      import { StatusBar } from "expo-status-bar";
      _39
      import "react-native-reanimated";
      _39
      _39
      import { useColorScheme } from "@/hooks/useColorScheme";
      _39
      import { OpenfortProvider } from "@openfort/react-native"; // Import the provider
      _39
      _39
      export default function RootLayout() {
      _39
      const colorScheme = useColorScheme();
      _39
      const [loaded] = useFonts({
      _39
      SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
      _39
      });
      _39
      _39
      // Wait for fonts to load before rendering the app
      _39
      if (!loaded) {
      _39
      return null;
      _39
      }
      _39
      _39
      return (
      _39
      <OpenfortProvider
      _39
      publishableKey={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
      }

  2. Adding Wallet Configuration:
    • To enable wallet creation and management, extend the provider with wallet configuration properties:


      _39
      import { OpenfortProvider, RecoveryMethod } from "@openfort/react-native"; // Add RecoveryMethod import
      _39
      _39
      export default function RootLayout() {
      _39
      const colorScheme = useColorScheme();
      _39
      const [loaded] = useFonts({
      _39
      SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
      _39
      });
      _39
      _39
      if (!loaded) {
      _39
      return null;
      _39
      }
      _39
      _39
      return (
      _39
      <OpenfortProvider
      _39
      publishableKey={Constants.expoConfig?.extra?.openfortPublishableKey}
      _39
      walletConfig={{
      _39
      // Shield service for secure key management
      _39
      _39
      shieldPublishableKey:
      _39
      Constants.expoConfig?.extra?.openfortShieldPublishableKey,
      _39
      _39
      // Allow users to recover wallets with password
      _39
      recoveryMethod: RecoveryMethod.PASSWORD,
      _39
      _39
      // Encryption key for additional security
      _39
      shieldEncryptionKey:
      _39
      Constants.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
      }

  3. Adding Blockchain Network Support:
    • Finally, specify which blockchain networks your app will support by adding the supportedChains configuration:


      _49
      export default function RootLayout() {
      _49
      const colorScheme = useColorScheme();
      _49
      const [loaded] = useFonts({
      _49
      SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
      _49
      });
      _49
      _49
      if (!loaded) {
      _49
      return null;
      _49
      }
      _49
      _49
      return (
      _49
      <OpenfortProvider
      _49
      publishableKey={Constants.expoConfig?.extra?.openfortPublishableKey}
      _49
      walletConfig={{
      _49
      shieldPublishableKey:
      _49
      Constants.expoConfig?.extra?.openfortShieldPublishableKey,
      _49
      recoveryMethod: RecoveryMethod.PASSWORD,
      _49
      shieldEncryptionKey:
      _49
      Constants.expoConfig?.extra?.openfortShieldEncryptionKey,
      _49
      }}
      _49
      supportedChains={[
      _49
      {
      _49
      // Avalanche Fuji testnet configuration
      _49
      id: 43113, // Chain ID for Avalanche Fuji
      _49
      name: "Avalanche Fuji",
      _49
      nativeCurrency: {
      _49
      name: "Avalanche", // Display name
      _49
      symbol: "AVAX", // Currency symbol
      _49
      decimals: 18, // Standard ERC-20 decimals
      _49
      },
      _49
      rpcUrls: {
      _49
      default: {
      _49
      // RPC endpoint for connecting to the network
      _49
      http: ["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

  1. Create a Protected Area for Authenticated Players:

    • Create a new folder called app/(tabs)/protected/.

    • Move all existing files from app/(tabs)/ into the new protected/ folder. Your file structure should change as follows:


      _16
      # Before
      _16
      app/
      _16
      └── (tabs)/
      _16
      ├── _layout.tsx
      _16
      ├── explore.tsx
      _16
      └── index.tsx
      _16
      _16
      # After
      _16
      app/
      _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

  2. Create Authentication Router:

    • Create a new layout file at app/(tabs)/_layout.tsx that handles routing between public and protected areas:


      _31
      import { useOpenfort } from "@openfort/react-native";
      _31
      import { Stack } from "expo-router";
      _31
      import { useEffect, useState } from "react";
      _31
      _31
      export default function AppLayout() {
      _31
      const { user } = useOpenfort(); // Get current user state
      _31
      const [isAuth, setIsAuth] = useState<boolean>(false);
      _31
      _31
      // Update authentication state when user changes
      _31
      useEffect(() => {
      _31
      if (user) {
      _31
      setIsAuth(true);
      _31
      } else {
      _31
      setIsAuth(false); // Reset to false when user logs out
      _31
      }
      _31
      }, [user]);
      _31
      _31
      return (
      _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

  1. Create Authentication Page:

    • Create a dedicated styles file for consistent authentication UI across your app.

    • Create constants/AuthStyles.ts:


      _164
      import { StyleSheet } from 'react-native';
      _164
      _164
      export const styles = StyleSheet.create({
      _164
      // Main container
      _164
      container: {
      _164
      flex: 1,
      _164
      backgroundColor: '#1a1a2e', // Dark theme background
      _164
      },
      _164
      _164
      // Content layout
      _164
      content: {
      _164
      flex: 1,
      _164
      justifyContent: "space-between",
      _164
      paddingHorizontal: 24,
      _164
      paddingVertical: 40,
      _164
      },
      _164
      _164
      // Header section
      _164
      header: {
      _164
      alignItems: "center",
      _164
      marginTop: 60,
      _164
      },
      _164
      logoContainer: {
      _164
      marginBottom: 24,
      _164
      },
      _164
      logo: {
      _164
      width: 80,
      _164
      height: 80,
      _164
      borderRadius: 40,
      _164
      backgroundColor: "rgba(255, 255, 255, 0.1)",
      _164
      justifyContent: "center",
      _164
      alignItems: "center",
      _164
      borderWidth: 2,
      _164
      borderColor: "rgba(255, 255, 255, 0.2)",
      _164
      },
      _164
      logoText: {
      _164
      fontSize: 40,
      _164
      },
      _164
      title: {
      _164
      fontSize: 24,
      _164
      color: '#ffffff',
      _164
      marginBottom: 8,
      _164
      fontWeight: "300",
      _164
      },
      _164
      appName: {
      _164
      fontSize: 32,
      _164
      fontWeight: "bold",
      _164
      color: '#ffffff',
      _164
      marginBottom: 12,
      _164
      textAlign: "center",
      _164
      },
      _164
      subtitle: {
      _164
      fontSize: 16,
      _164
      color: "rgba(255, 255, 255, 0.7)", // Semi-transparent white
      _164
      textAlign: "center",
      _164
      lineHeight: 22,
      _164
      },
      _164
      _164
      // Authentication section
      _164
      authSection: {
      _164
      flex: 1,
      _164
      justifyContent: "center",
      _164
      paddingVertical: 40,
      _164
      },
      _164
      _164
      // Guest login button
      _164
      guestButton: {
      _164
      borderRadius: 16,
      _164
      overflow: "hidden",
      _164
      marginBottom: 32,
      _164
      // Shadow effects for iOS
      _164
      shadowColor: "#ff6b6b",
      _164
      shadowOffset: { width: 0, height: 4 },
      _164
      shadowOpacity: 0.3,
      _164
      shadowRadius: 8,
      _164
      // Elevation for Android
      _164
      elevation: 8,
      _164
      },
      _164
      buttonGradient: {
      _164
      flexDirection: "row",
      _164
      alignItems: "center",
      _164
      justifyContent: "center",
      _164
      paddingVertical: 18,
      _164
      paddingHorizontal: 24,
      _164
      },
      _164
      guestButtonIcon: {
      _164
      fontSize: 20,
      _164
      marginRight: 12,
      _164
      },
      _164
      primaryButtonText: {
      _164
      color: '#ffffff',
      _164
      fontSize: 18,
      _164
      fontWeight: "600",
      _164
      letterSpacing: 0.5,
      _164
      },
      _164
      _164
      // Divider between auth methods
      _164
      dividerContainer: {
      _164
      flexDirection: "row",
      _164
      alignItems: "center",
      _164
      marginVertical: 24,
      _164
      },
      _164
      dividerLine: {
      _164
      flex: 1,
      _164
      height: 1,
      _164
      backgroundColor: "rgba(255, 255, 255, 0.2)", // Light divider line
      _164
      },
      _164
      dividerText: {
      _164
      color: "rgba(255, 255, 255, 0.6)",
      _164
      fontSize: 14,
      _164
      fontWeight: "500",
      _164
      marginHorizontal: 16,
      _164
      letterSpacing: 1,
      _164
      },
      _164
      _164
      // OAuth section
      _164
      oauthContainer: {
      _164
      gap: 16,
      _164
      },
      _164
      oauthButton: {
      _164
      backgroundColor: "rgba(255, 255, 255, 0.95)",
      _164
      borderRadius: 12,
      _164
      overflow: "hidden",
      _164
      // Shadow effects
      _164
      shadowColor: "#000",
      _164
      shadowOffset: { width: 0, height: 2 },
      _164
      shadowOpacity: 0.1,
      _164
      shadowRadius: 4,
      _164
      elevation: 4,
      _164
      },
      _164
      oauthButtonContent: {
      _164
      flexDirection: "row",
      _164
      alignItems: "center",
      _164
      justifyContent: "center",
      _164
      paddingVertical: 16,
      _164
      paddingHorizontal: 24,
      _164
      },
      _164
      googleIcon: {
      _164
      fontSize: 20,
      _164
      marginRight: 12,
      _164
      },
      _164
      oauthButtonText: {
      _164
      color: "#333333",
      _164
      fontSize: 16,
      _164
      fontWeight: "600",
      _164
      letterSpacing: 0.3,
      _164
      },
      _164
      _164
      // Error display
      _164
      errorContainer: {
      _164
      backgroundColor: "rgba(255, 59, 48, 0.1)",
      _164
      borderRadius: 8,
      _164
      padding: 12,
      _164
      marginTop: 16,
      _164
      borderWidth: 1,
      _164
      borderColor: "rgba(255, 59, 48, 0.3)",
      _164
      },
      _164
      errorText: {
      _164
      color: "#ff6b6b",
      _164
      fontSize: 14,
      _164
      textAlign: "center",
      _164
      fontWeight: "500",
      _164
      },
      _164
      });

    • Update your app/(tabs)/index.tsx to create a comprehensive login screen with both guest and OAuth authentication options:


      _112
      import { OAuthProvider, useGuestAuth, useOAuth } from "@openfort/react-native";
      _112
      import { LinearGradient } from "expo-linear-gradient";
      _112
      import { styles } from '@/constants/AuthStyles';
      _112
      import {
      _112
      Alert,
      _112
      SafeAreaView,
      _112
      StatusBar,
      _112
      Text,
      _112
      TouchableOpacity,
      _112
      View,
      _112
      } from "react-native";
      _112
      _112
      export default function AuthScreen() {
      _112
      // Openfort authentication hooks
      _112
      const { signUpGuest } = useGuestAuth();
      _112
      const { initOAuth, error } = useOAuth();
      _112
      _112
      // Handle guest login with error handling
      _112
      const handleGuestLogin = async () => {
      _112
      try {
      _112
      await signUpGuest();
      _112
      // User will be automatically redirected to protected routes
      _112
      } catch (err) {
      _112
      console.error("Guest login error:", err);
      _112
      Alert.alert("Login Error", "Failed to login as guest. Please try again.");
      _112
      }
      _112
      };
      _112
      _112
      // Handle OAuth login with error handling
      _112
      const handleOAuthLogin = async (provider: string) => {
      _112
      try {
      _112
      await initOAuth({ provider: provider as OAuthProvider });
      _112
      // User will be automatically redirected to protected routes
      _112
      } catch (err) {
      _112
      console.error(`OAuth login error with ${provider}:`, err);
      _112
      Alert.alert(
      _112
      "Login Error",
      _112
      `Failed to login with ${provider}. Please try again.`
      _112
      );
      _112
      }
      _112
      };
      _112
      _112
      return (
      _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}>
      _112
      Your 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
      _112
      style={styles.guestButton}
      _112
      onPress={handleGuestLogin}
      _112
      activeOpacity={0.8}
      _112
      >
      _112
      <LinearGradient
      _112
      colors={["#ff6b6b", "#ee5a52"]} // Red gradient
      _112
      style={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
      _112
      style={styles.oauthButton}
      _112
      onPress={() => handleOAuthLogin("google")}
      _112
      activeOpacity={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.tsx updated, this will ensure that the first screen a player sees when they access the platform is the login page:

  2. 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 IDs and Client Secret from 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:


      _10
      npm run ios

    • Your application should look like this after executing the above commmand:

      auth.png

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.

homescreen.png

  1. Create the Game Component:

    • Create constants/GameStyles.ts and paste the game style:


      _101
      import { StyleSheet, Dimensions } from 'react-native';
      _101
      _101
      const { width } = Dimensions.get('window');
      _101
      _101
      export const styles = StyleSheet.create({
      _101
      // Main container
      _101
      container: {
      _101
      alignItems: "center",
      _101
      padding: 20,
      _101
      backgroundColor: '#f8f9fa', // Light background
      _101
      },
      _101
      _101
      // Game title
      _101
      title: {
      _101
      fontSize: 24,
      _101
      fontWeight: "bold",
      _101
      color: "#333333", // Dark text for readability
      _101
      marginBottom: 20,
      _101
      textAlign: "center",
      _101
      },
      _101
      _101
      // Countdown timer
      _101
      countdownContainer: {
      _101
      width: 60,
      _101
      height: 60,
      _101
      borderRadius: 30,
      _101
      backgroundColor: "#ff6b6b", // Red background for urgency
      _101
      justifyContent: "center",
      _101
      alignItems: "center",
      _101
      marginBottom: 20,
      _101
      },
      _101
      countdown: {
      _101
      fontSize: 28,
      _101
      fontWeight: "bold",
      _101
      color: "#ffffff",
      _101
      },
      _101
      _101
      // Numbers grid layout
      _101
      numbersContainer: {
      _101
      flexDirection: "row",
      _101
      flexWrap: "wrap",
      _101
      justifyContent: "center",
      _101
      gap: 10,
      _101
      marginBottom: 30,
      _101
      },
      _101
      numberCard: {
      _101
      width: (width - 80) / 3, // Responsive width for 3 cards per row
      _101
      height: 80,
      _101
      backgroundColor: "#f0f0f0",
      _101
      borderRadius: 8,
      _101
      justifyContent: "center",
      _101
      alignItems: "center",
      _101
      borderWidth: 2,
      _101
      borderColor: "#ddd",
      _101
      },
      _101
      // Selected target number styling
      _101
      targetCard: {
      _101
      backgroundColor: "#3CB371", // Green for selected target
      _101
      borderColor: "#7CFC00",
      _101
      },
      _101
      // User's guess selection
      _101
      selectedCard: {
      _101
      backgroundColor: "#ff6b6b", // Red for selected guess
      _101
      borderColor: "#ff6b6b",
      _101
      },
      _101
      numberText: {
      _101
      fontSize: 32,
      _101
      fontWeight: "bold",
      _101
      color: "#333333",
      _101
      },
      _101
      _101
      // Game instructions
      _101
      instructionContainer: {
      _101
      backgroundColor: "rgba(0, 0, 0, 0.05)", // Subtle background
      _101
      padding: 15,
      _101
      borderRadius: 8,
      _101
      marginHorizontal: 20,
      _101
      },
      _101
      instructionText: {
      _101
      fontSize: 16,
      _101
      color: "#333333",
      _101
      textAlign: "center",
      _101
      },
      _101
      _101
      // Reset/New Game button
      _101
      resetButton: {
      _101
      marginTop: 20,
      _101
      backgroundColor: "rgba(46, 213, 115, 0.2)",
      _101
      borderRadius: 12,
      _101
      paddingVertical: 12,
      _101
      paddingHorizontal: 24,
      _101
      borderWidth: 1,
      _101
      borderColor: "#2ed573",
      _101
      },
      _101
      resetButtonText: {
      _101
      color: "#2ed573",
      _101
      fontSize: 16,
      _101
      fontWeight: "600",
      _101
      textAlign: "center",
      _101
      },
      _101
      });

      • Create components/NumberPreview.tsx and paste the following code:


        _213
        import React, { useEffect, useState } from "react";
        _213
        import {
        _213
        Text,
        _213
        TouchableOpacity,
        _213
        View,
        _213
        } from "react-native";
        _213
        import { styles } from '../constants/GameStyles';
        _213
        _213
        interface NumberPreviewProps {
        _213
        numbers: number[];
        _213
        onNumberSelect: (selectedNumber: number, isCorrect: boolean) => void;
        _213
        onGameReset?: () => void;
        _213
        }
        _213
        _213
        export const NumberPreview: React.FC<NumberPreviewProps> = ({
        _213
        numbers,
        _213
        onNumberSelect,
        _213
        onGameReset,
        _213
        }) => {
        _213
        // Game state management
        _213
        const [countdown, setCountdown] = useState(5);
        _213
        const [gamePhase, setGamePhase] = useState<
        _213
        "select" | "ready" | "shuffle" | "guess" | "failed"
        _213
        >("select");
        _213
        const [selectedTargetNumber, setSelectedTargetNumber] = useState<number | null>(null);
        _213
        const [shuffledNumbers, setShuffledNumbers] = useState(numbers);
        _213
        const [correctPosition, setCorrectPosition] = useState(0);
        _213
        const [selectedCard, setSelectedCard] = useState<number | null> (null);
        _213
        _213
        // Countdown timer effect for ready phase
        _213
        useEffect(() => {
        _213
        if (gamePhase === "ready" && countdown > 0) {
        _213
        const timer = setTimeout(() => {
        _213
        setCountdown(countdown - 1);
        _213
        }, 1000);
        _213
        return () => clearTimeout(timer);
        _213
        } else if (countdown === 0 && gamePhase === "ready") {
        _213
        startShufflePhase();
        _213
        }
        _213
        }, [countdown, gamePhase]);
        _213
        _213
        // Handle player selecting their target number
        _213
        const handleTargetNumberSelect = (number: number) => {
        _213
        if (gamePhase !== "select") return;
        _213
        _213
        setSelectedTargetNumber(number);
        _213
        setGamePhase("ready");
        _213
        setCountdown(5); // Start 5-second countdown
        _213
        };
        _213
        _213
        // Begin the shuffle phase
        _213
        const startShufflePhase = () => {
        _213
        setGamePhase("shuffle");
        _213
        _213
        // Brief delay for visual feedback before shuffle
        _213
        setTimeout(() => {
        _213
        shuffleNumbers();
        _213
        }, 1000);
        _213
        };
        _213
        _213
        // Shuffle the numbers and track target position
        _213
        const shuffleNumbers = () => {
        _213
        // Fisher-Yates shuffle algorithm
        _213
        const shuffled = [...numbers];
        _213
        for (let i = shuffled.length - 1; i > 0; i--) {
        _213
        const 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
        _213
        const newTargetIndex = shuffled.indexOf(selectedTargetNumber!);
        _213
        _213
        setShuffledNumbers(shuffled);
        _213
        setCorrectPosition(newTargetIndex);
        _213
        setGamePhase("guess");
        _213
        };
        _213
        _213
        // Handle player's guess
        _213
        const handleCardPress = (index: number) => {
        _213
        if (gamePhase !== "guess" || selectedCard !== null) return;
        _213
        _213
        setSelectedCard(index);
        _213
        const isCorrect = index === correctPosition;
        _213
        _213
        if (!isCorrect) {
        _213
        setGamePhase("failed");
        _213
        }
        _213
        _213
        // Delay feedback to show selection
        _213
        setTimeout(() => {
        _213
        onNumberSelect(selectedTargetNumber!, isCorrect);
        _213
        }, 500);
        _213
        };
        _213
        _213
        // Reset game to initial state
        _213
        const resetGame = () => {
        _213
        setCountdown(5);
        _213
        setGamePhase("select");
        _213
        setSelectedTargetNumber(null);
        _213
        setSelectedCard(null);
        _213
        setShuffledNumbers(numbers);
        _213
        setCorrectPosition(0);
        _213
        _213
        // Call optional reset callback
        _213
        if (onGameReset) {
        _213
        onGameReset();
        _213
        }
        _213
        };
        _213
        _213
        // Dynamic title based on game phase
        _213
        const getPhaseTitle = () => {
        _213
        switch (gamePhase) {
        _213
        case "select":
        _213
        return "Choose Your Lucky Number!";
        _213
        case "ready":
        _213
        return `Get Ready! Your number: ${selectedTargetNumber}`;
        _213
        case "shuffle":
        _213
        return "Shuffling...";
        _213
        case "guess":
        _213
        return `Find: ${selectedTargetNumber}`;
        _213
        case "failed":
        _213
        return "Game Over!";
        _213
        default:
        _213
        return "";
        _213
        }
        _213
        };
        _213
        _213
        // Get current number array based on game phase
        _213
        const getCurrentNumbers = () => {
        _213
        if (gamePhase === "select" || gamePhase === "ready") {
        _213
        return numbers; // Show original positions
        _213
        }
        _213
        return shuffledNumbers; // Show shuffled positions
        _213
        };
        _213
        _213
        // Determine whether to show number or hide it
        _213
        const showNumber = (number: number, index: number) => {
        _213
        // Always show during select, ready, and failed phases
        _213
        if (gamePhase === "select" || gamePhase === "ready" || gamePhase === "failed") {
        _213
        return true;
        _213
        }
        _213
        // During guess phase, only show if card is selected
        _213
        return selectedCard === index;
        _213
        };
        _213
        _213
        return (
        _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) => {
        _213
        const isTargetNumber =
        _213
        selectedTargetNumber === number && gamePhase !== "guess";
        _213
        const isSelected = selectedCard === index;
        _213
        _213
        return (
        _213
        <TouchableOpacity
        _213
        key={`${gamePhase}-${index}`}
        _213
        style={[
        _213
        styles.numberCard,
        _213
        isTargetNumber && styles.targetCard, // Highlight selected target
        _213
        isSelected && styles.selectedCard, // Highlight player's guess
        _213
        ]}
        _213
        onPress={() => {
        _213
        if (gamePhase === "select") {
        _213
        handleTargetNumberSelect(number);
        _213
        } else if (gamePhase === "guess") {
        _213
        handleCardPress(index);
        _213
        }
        _213
        }}
        _213
        disabled={
        _213
        gamePhase === "ready" ||
        _213
        gamePhase === "shuffle" ||
        _213
        gamePhase === "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
        };

  2. 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:


      _178
      import { useOpenfort, useWallets } from "@openfort/react-native";
      _178
      import { useState } from "react";
      _178
      import { Alert, StyleSheet, Text, View } from "react-native";
      _178
      import { SafeAreaView } from "react-native-safe-area-context";
      _178
      import { NumberPreview } from "../../../components/ NumberPreview"; // Updated import
      _178
      _178
      export default function HomeScreen() {
      _178
      const { user } = useOpenfort(); // Get authenticated user
      _178
      const { activeWallet } = useWallets(); // Get active wallet state
      _178
      const [gameNumbers] = useState([7, 23, 91, 45, 8]); // Static game numbers
      _178
      _178
      // Handle game result with user feedback
      _178
      const handleNumberSelect = (selectedNumber: number, isCorrect: boolean) => {
      _178
      if (isCorrect) {
      _178
      Alert.alert(
      _178
      "🎉 Congratulations!",
      _178
      `You found ${selectedNumber} in the right position!`
      _178
      );
      _178
      } else {
      _178
      Alert.alert(
      _178
      "❌ Close!",
      _178
      `${selectedNumber} was in a different position. Try again!`
      _178
      );
      _178
      }
      _178
      };
      _178
      _178
      _178
      return (
      _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
      _178
      numbers={gameNumbers}
      _178
      onNumberSelect={handleNumberSelect}
      _178
      onGameReset={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}>
      _178
      Connect 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
      _178
      const styles = StyleSheet.create({
      _178
      container: {
      _178
      flex: 1,
      _178
      backgroundColor: '#f8f9fa',
      _178
      },
      _178
      content: {
      _178
      flex: 1,
      _178
      paddingHorizontal: 16,
      _178
      },
      _178
      _178
      // Header styles
      _178
      header: {
      _178
      paddingVertical: 20,
      _178
      alignItems: 'center',
      _178
      },
      _178
      title: {
      _178
      fontSize: 28,
      _178
      fontWeight: "bold",
      _178
      color: '#333',
      _178
      marginBottom: 4,
      _178
      },
      _178
      subtitle: {
      _178
      fontSize: 16,
      _178
      color: '#666',
      _178
      },
      _178
      _178
      // User info display
      _178
      userInfo: {
      _178
      backgroundColor: 'rgba(0, 0, 0, 0.05)',
      _178
      borderRadius: 8,
      _178
      padding: 12,
      _178
      marginBottom: 20,
      _178
      flexDirection: 'row',
      _178
      alignItems: 'center',
      _178
      },
      _178
      userLabel: {
      _178
      fontSize: 14,
      _178
      fontWeight: '600',
      _178
      color: '#666',
      _178
      marginRight: 8,
      _178
      },
      _178
      userId: {
      _178
      fontSize: 14,
      _178
      fontFamily: 'monospace', // Monospace for ID display
      _178
      color: '#333',
      _178
      flex: 1,
      _178
      },
      _178
      _178
      // Game container
      _178
      gameContainer: {
      _178
      flex: 1,
      _178
      justifyContent: 'center',
      _178
      },
      _178
      _178
      // No wallet state styles
      _178
      noWalletContainer: {
      _178
      backgroundColor: 'rgba(255, 255, 255, 0.9)',
      _178
      borderRadius: 16,
      _178
      padding: 32,
      _178
      alignItems: 'center',
      _178
      marginHorizontal: 16,
      _178
      borderWidth: 1,
      _178
      borderColor: 'rgba(0, 0, 0, 0.1)',
      _178
      // Shadow for iOS
      _178
      shadowColor: '#000',
      _178
      shadowOffset: { width: 0, height: 2 },
      _178
      shadowOpacity: 0.1,
      _178
      shadowRadius: 8,
      _178
      // Elevation for Android
      _178
      elevation: 4,
      _178
      },
      _178
      noWalletIconContainer: {
      _178
      width: 80,
      _178
      height: 80,
      _178
      borderRadius: 40,
      _178
      backgroundColor: 'rgba(0, 0, 0, 0.05)',
      _178
      justifyContent: 'center',
      _178
      alignItems: 'center',
      _178
      marginBottom: 20,
      _178
      },
      _178
      noWalletIcon: {
      _178
      fontSize: 40,
      _178
      },
      _178
      noWalletTitle: {
      _178
      fontSize: 24,
      _178
      fontWeight: 'bold',
      _178
      color: '#333',
      _178
      marginBottom: 12,
      _178
      textAlign: 'center',
      _178
      },
      _178
      noWalletMessage: {
      _178
      fontSize: 16,
      _178
      color: '#666',
      _178
      textAlign: 'center',
      _178
      lineHeight: 24,
      _178
      marginBottom: 16,
      _178
      },
      _178
      noWalletHint: {
      _178
      fontSize: 14,
      _178
      color: '#888',
      _178
      textAlign: 'center',
      _178
      fontStyle: 'italic',
      _178
      },
      _178
      });

    • Now access the application in your simulator again,and you'd see this when a player logs in:

d11.png

  • With the Openfort useOpenfort hook, 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

  1. Create the app/(tabs)/protected/settings.tsx file and add the following code:


    _348
    import { useOpenfort, useWallets } from "@openfort/react-native";
    _348
    import { useRouter } from "expo-router";
    _348
    import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native";
    _348
    import { SafeAreaView } from "react-native-safe-area-context";
    _348
    _348
    export default function SettingsScreen() {
    _348
    const router = useRouter();
    _348
    const { logout } = useOpenfort(); // Get logout function from Openfort
    _348
    _348
    // Wallet management hooks with error handling
    _348
    const { wallets, setActiveWallet, createWallet, activeWallet, isCreating } = useWallets({
    _348
    onError: (error) => {
    _348
    console.error("Wallet Error:", error);
    _348
    Alert.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
    _348
    const recoveryPassword = "test123";
    _348
    _348
    // Handle user logout with navigation
    _348
    const handleLogout = () => {
    _348
    Alert.alert(
    _348
    "Confirm Logout",
    _348
    "Are you sure you want to sign out?",
    _348
    [
    _348
    { text: "Cancel", style: "cancel" },
    _348
    {
    _348
    text: "Sign Out",
    _348
    style: "destructive",
    _348
    onPress: () => {
    _348
    logout();
    _348
    router.replace("/(tabs)"); // Navigate back to auth screen
    _348
    },
    _348
    },
    _348
    ]
    _348
    );
    _348
    };
    _348
    _348
    // Create new wallet with error handling
    _348
    const handleCreateWallet = async () => {
    _348
    try {
    _348
    await createWallet({
    _348
    recoveryPassword: recoveryPassword,
    _348
    });
    _348
    Alert.alert("Success", "New wallet created successfully!");
    _348
    } catch (error) {
    _348
    console.error("Wallet creation failed:", error);
    _348
    Alert.alert("Error", "Failed to create wallet. Please try again.");
    _348
    }
    _348
    };
    _348
    _348
    // Set active wallet with comprehensive error handling
    _348
    const handleSetActiveWallet = async (walletAddress: string) => {
    _348
    try {
    _348
    await setActiveWallet({
    _348
    address: walletAddress,
    _348
    chainId: 43113, // Avalanche Fuji testnet
    _348
    recoveryPassword: recoveryPassword,
    _348
    onSuccess: () => {
    _348
    Alert.alert("Success", `Wallet ${walletAddress.slice(0, 6)}...${walletAddress.slice(-4)} is now active`);
    _348
    },
    _348
    onError: (error) => {
    _348
    Alert.alert("Error", `Failed to set active wallet: ${error.message}`);
    _348
    },
    _348
    });
    _348
    } catch (error) {
    _348
    console.error("Set active wallet failed:", error);
    _348
    Alert.alert("Error", "Failed to activate wallet. Please try again.");
    _348
    }
    _348
    };
    _348
    _348
    return (
    _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}>
    _348
    Tap a wallet to make it active. Active wallets can be used for transactions.
    _348
    </Text>
    _348
    ) : (
    _348
    <Text style={styles.walletDescription}>
    _348
    Create 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) => {
    _348
    const isActive = activeWallet?.address === wallet.address;
    _348
    const displayAddress = `${wallet.address.slice(0, 8)}...${wallet.address.slice(-6)}`;
    _348
    _348
    return (
    _348
    <View key={wallet.address + index} style={styles.walletItem}>
    _348
    <TouchableOpacity
    _348
    style={[
    _348
    styles.walletButton,
    _348
    isActive && styles.activeWalletButton,
    _348
    ]}
    _348
    onPress={() => handleSetActiveWallet(wallet.address)}
    _348
    disabled={isActive || wallet.isConnecting}
    _348
    >
    _348
    <View style={styles.walletButtonContent}>
    _348
    <Text style={[
    _348
    styles.walletAddress,
    _348
    isActive && 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
    _348
    style={[
    _348
    styles.createWalletButton,
    _348
    isCreating && styles.disabledButton,
    _348
    ]}
    _348
    onPress={handleCreateWallet}
    _348
    disabled={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
    _348
    const styles = StyleSheet.create({
    _348
    container: {
    _348
    flex: 1,
    _348
    backgroundColor: '#f8f9fa',
    _348
    },
    _348
    content: {
    _348
    flex: 1,
    _348
    paddingHorizontal: 16,
    _348
    },
    _348
    _348
    // Header
    _348
    header: {
    _348
    paddingVertical: 24,
    _348
    },
    _348
    title: {
    _348
    fontSize: 28,
    _348
    fontWeight: "bold",
    _348
    color: '#333',
    _348
    marginBottom: 4,
    _348
    },
    _348
    subtitle: {
    _348
    fontSize: 16,
    _348
    color: '#666',
    _348
    },
    _348
    _348
    // Wallet Section
    _348
    walletContainer: {
    _348
    marginBottom: 32,
    _348
    },
    _348
    walletSectionTitle: {
    _348
    fontSize: 20,
    _348
    fontWeight: "bold",
    _348
    color: '#333',
    _348
    marginBottom: 8,
    _348
    },
    _348
    walletDescription: {
    _348
    fontSize: 14,
    _348
    color: '#666',
    _348
    marginBottom: 20,
    _348
    lineHeight: 20,
    _348
    },
    _348
    walletList: {
    _348
    marginBottom: 20,
    _348
    },
    _348
    walletItem: {
    _348
    marginBottom: 12,
    _348
    },
    _348
    walletButton: {
    _348
    backgroundColor: '#fff',
    _348
    borderRadius: 12,
    _348
    padding: 16,
    _348
    borderWidth: 1,
    _348
    borderColor: '#e0e0e0',
    _348
    shadowColor: '#000',
    _348
    shadowOffset: { width: 0, height: 1 },
    _348
    shadowOpacity: 0.1,
    _348
    shadowRadius: 2,
    _348
    elevation: 2,
    _348
    },
    _348
    activeWalletButton: {
    _348
    borderColor: '#28a745',
    _348
    backgroundColor: '#f8fff9',
    _348
    },
    _348
    walletButtonContent: {
    _348
    flexDirection: 'row',
    _348
    alignItems: 'center',
    _348
    justifyContent: 'space-between',
    _348
    },
    _348
    walletAddress: {
    _348
    fontSize: 16,
    _348
    fontFamily: 'monospace',
    _348
    color: '#333',
    _348
    fontWeight: '500',
    _348
    },
    _348
    activeWalletText: {
    _348
    color: '#28a745',
    _348
    },
    _348
    activeBadge: {
    _348
    backgroundColor: '#28a745',
    _348
    borderRadius: 12,
    _348
    paddingHorizontal: 8,
    _348
    paddingVertical: 4,
    _348
    },
    _348
    activeBadgeText: {
    _348
    color: '#fff',
    _348
    fontSize: 10,
    _348
    fontWeight: 'bold',
    _348
    },
    _348
    connectingText: {
    _348
    color: '#666',
    _348
    fontSize: 12,
    _348
    fontStyle: "italic",
    _348
    marginTop: 4,
    _348
    textAlign: 'center',
    _348
    },
    _348
    _348
    // Create Wallet Button
    _348
    createWalletButton: {
    _348
    backgroundColor: '#007AFF',
    _348
    borderRadius: 12,
    _348
    paddingVertical: 16,
    _348
    paddingHorizontal: 24,
    _348
    alignItems: 'center',
    _348
    shadowColor: '#000',
    _348
    shadowOffset: { width: 0, height: 2 },
    _348
    shadowOpacity: 0.1,
    _348
    shadowRadius: 4,
    _348
    elevation: 3,
    _348
    },
    _348
    disabledButton: {
    _348
    backgroundColor: '#ccc',
    _348
    opacity: 0.6,
    _348
    },
    _348
    createWalletButtonText: {
    _348
    color: '#fff',
    _348
    fontSize: 16,
    _348
    fontWeight: '600',
    _348
    },
    _348
    _348
    // Additional Settings
    _348
    additionalSettings: {
    _348
    flex: 1,
    _348
    },
    _348
    sectionTitle: {
    _348
    fontSize: 18,
    _348
    fontWeight: 'bold',
    _348
    color: '#333',
    _348
    marginBottom: 16,
    _348
    },
    _348
    settingItem: {
    _348
    backgroundColor: '#fff',
    _348
    borderRadius: 12,
    _348
    padding: 16,
    _348
    marginBottom: 8,
    _348
    borderWidth: 1,
    _348
    borderColor: '#e0e0e0',
    _348
    },
    _348
    settingItemText: {
    _348
    fontSize: 16,
    _348
    fontWeight: '500',
    _348
    color: '#333',
    _348
    marginBottom: 4,
    _348
    },
    _348
    settingItemSubtext: {
    _348
    fontSize: 14,
    _348
    color: '#666',
    _348
    },
    _348
    _348
    // Logout
    _348
    logoutContainer: {
    _348
    paddingVertical: 24,
    _348
    paddingHorizontal: 16,
    _348
    },
    _348
    logoutButton: {
    _348
    backgroundColor: '#ff3b30',
    _348
    borderRadius: 12,
    _348
    paddingVertical: 16,
    _348
    alignItems: 'center',
    _348
    shadowColor: '#000',
    _348
    shadowOffset: { width: 0, height: 2 },
    _348
    shadowOpacity: 0.1,
    _348
    shadowRadius: 4,
    _348
    elevation: 3,
    _348
    },
    _348
    logoutText: {
    _348
    color: '#fff',
    _348
    fontSize: 16,
    _348
    fontWeight: '600',
    _348
    },
    _348
    });

  2. Configure Tab Navigation:

    • Update the protected area's tab layout to include the settings screen. Update app/(tabs)/protected/_layout.tsx:

    _53
    import { Tabs } from "expo-router";
    _53
    import React from "react";
    _53
    import { Platform } from "react-native";
    _53
    _53
    import { HapticTab } from "@/components/HapticTab";
    _53
    import { IconSymbol } from "@/components/ui/IconSymbol";
    _53
    import TabBarBackground from "@/components/ui/TabBarBackground";
    _53
    import { Colors } from "@/constants/Colors";
    _53
    import { useColorScheme } from "@/hooks/useColorScheme";
    _53
    _53
    export default function TabLayout() {
    _53
    const colorScheme = useColorScheme();
    _53
    _53
    return (
    _53
    <Tabs
    _53
    screenOptions={{
    _53
    tabBarActiveTintColor: Colors[colorScheme ?? "light"].tint,
    _53
    headerShown: false,
    _53
    tabBarButton: HapticTab,
    _53
    tabBarBackground: TabBarBackground,
    _53
    tabBarStyle: Platform.select({
    _53
    ios: {
    _53
    // Floating tab bar on iOS
    _53
    position: "absolute",
    _53
    },
    _53
    default: {},
    _53
    }),
    _53
    }}
    _53
    >
    _53
    {/* Home Tab */}
    _53
    <Tabs.Screen
    _53
    name="index"
    _53
    options={{
    _53
    title: "Home",
    _53
    tabBarIcon: ({ color }) => (
    _53
    <IconSymbol size={28} name="house.fill" color={color} />
    _53
    ),
    _53
    }}
    _53
    />
    _53
    _53
    {/* Settings Tab */}
    _53
    <Tabs.Screen
    _53
    name="settings"
    _53
    options={{
    _53
    title: "Settings",
    _53
    tabBarIcon: ({ color }) => (
    _53
    <IconSymbol size={28} name="gear" color={color} />
    _53
    ),
    _53
    }}
    _53
    />
    _53
    </Tabs>
    _53
    );
    _53
    }

  • Add the gear icon mapping for consistent cross-platform display. Update components/ui/IconSymbol.tsx:


    _51
    // Fallback for using MaterialIcons on Android and web.
    _51
    import MaterialIcons from "@expo/vector-icons/MaterialIcons";
    _51
    import { SymbolViewProps, SymbolWeight } from "expo-symbols";
    _51
    import { ComponentProps } from "react";
    _51
    import { OpaqueColorValue, type StyleProp, type TextStyle } from "react-native";
    _51
    _51
    type IconMapping = Record<
    _51
    SymbolViewProps["name"],
    _51
    ComponentProps<typeof MaterialIcons>["name"]
    _51
    >;
    _51
    type 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
    */
    _51
    const MAPPING = {
    _51
    "house.fill": "home",
    _51
    "paperplane.fill": "send",
    _51
    "chevron.left.forwardslash.chevron.right": "code",
    _51
    "chevron.right": "chevron-right",
    _51
    gear: "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
    */
    _51
    export function IconSymbol({
    _51
    name,
    _51
    size = 24,
    _51
    color,
    _51
    style,
    _51
    weight,
    _51
    }: {
    _51
    name: IconSymbolName;
    _51
    size?: number;
    _51
    color: string | OpaqueColorValue;
    _51
    style?: StyleProp<TextStyle>;
    _51
    weight?: SymbolWeight;
    _51
    }) {
    _51
    return (
    _51
    <MaterialIcons
    _51
    color={color}
    _51
    size={size}
    _51
    name={MAPPING[name]}
    _51
    style={style}
    _51
    />
    _51
    );
    _51
    }

  • Delete the app/(tabs)/protected/explore.tsx screen since it's no longer used:


    _10
    rm app/(tabs)/protected/explore.tsx

  • Navigate to the settings page to add new wallets or signout as seen below:

d13.png

  • 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:

  1. Open the Openfort Dashboard and navigate to the Users tab in the left sidebar.

  2. Select a Account from the list to view their profile:

  3. 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 supportedChains array 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!

Share this article

Sign up for Openfort