Expo Upload Guide

Learn how to prepare and upload your Expo web games

Back to guides

Preparing Your Expo Web Game

Expo is a framework and platform for universal React applications. You can use it to build games that run on web, iOS, and Android from a single codebase. Our platform supports Expo web builds.

Set Up Your Expo Project

If you haven't already, create a new Expo project:

# Install the Expo CLI
yarn global add expo-cli

# Create a new Expo project
expo init my-game

# Choose a template (e.g., blank, blank (TypeScript), or tabs)
# Navigate to your project
cd my-game

Note: We recommend using yarn as your package manager for Expo projects.

Configure Your Project for Web

Ensure your project is set up for web development:

# Install web dependencies
yarn add react-native-web react-dom @expo/webpack-config

Update your app.json to include web configuration:

{
  "expo": {
    "name": "My Game",
    "slug": "my-game",
    "version": "1.0.0",
    "orientation": "portrait",
    "platforms": ["ios", "android", "web"],
    "web": {
      "favicon": "./assets/favicon.png"
    },
    // ... other configurations
  }
}

Develop Your Game

Create your game using React Native components. Here's a simple example of a game component:

import React, { useState, useEffect } from 'react';
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';

export default function Game() {
  const [score, setScore] = useState(0);
  
  const handlePress = () => {
    setScore(score + 1);
  };
  
  return (
    <View style={styles.container}>
      <Text style={styles.title}>My Expo Game</Text>
      <Text style={styles.score}>Score: {score}</Text>
      <TouchableOpacity style={styles.button} onPress={handlePress}>
        <Text style={styles.buttonText}>Click Me!</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: '#f0f0f0',
  },
  title: {
    fontSize: 24,
    fontWeight: 'bold',
    marginBottom: 20,
  },
  score: {
    fontSize: 18,
    marginBottom: 30,
  },
  button: {
    backgroundColor: '#4a6cf7',
    padding: 15,
    borderRadius: 8,
  },
  buttonText: {
    color: 'white',
    fontSize: 16,
    fontWeight: 'bold',
  },
});

Test Your Game in Web Mode

Run your game in web mode to test it:

# Start the web development server
yarn web

This will open your game in a web browser. Test all features to ensure they work correctly in the web environment.

Build Your Game for Web

Create a production build of your game for web:

# Build for web
expo build:web

This will create a web-build directory with the following structure:

web-build/
  ├── _expo/
  ├── assets/
  ├── index.html
  ├── not-found.html
  ├── _sitemap.html
  └── ...

Note: The _expo folder contains Expo-specific files needed for your game to run correctly.

Test Your Production Build

Before uploading, test your production build to ensure everything works correctly:

# Using a simple HTTP server
npx serve web-build

Check that all game features, assets, and interactions work as expected.

Zip Your Build

Compress your web-build directory into a ZIP file:

# On macOS/Linux
cd web-build
zip -r ../my-game.zip ./*

# On Windows
# Right-click the web-build folder > Send to > Compressed (zipped) folder

Important: Make sure to zip the contents of the web-build folder, not the web-build folder itself.

Common Issues and Solutions

Missing Assets

If your game assets aren't loading:

Platform-Specific Code

If your game has platform-specific code:

Web Performance

To improve your game's web performance:

Implementing Game Session Tracking

All games uploaded to our platform must implement session tracking functionality. This allows us to track player sessions and maintain leaderboards for your game.

Important: Your game bundle must include API calls to our session tracking endpoints. Our validation system will check for these calls during the upload process. Games without proper session tracking implementation will be rejected.

Option 1: Use Our SDK (Recommended)

For Expo web games, you can use our JavaScript SDK by adding it to your project:

Step 1: Add the SDK to your project

Create a public folder in your project root (if it doesn't exist) and copy our SDK file into it:

mkdir -p public
cp /path/to/game-session-tracker.js public/

You can download the SDK here or view an example implementation.

Then, in your app.json, add the following to ensure the SDK is included in your web build:

{
  "expo": {
    "web": {
      "publicPath": "/",
      "build": {
        "babel": {
          "include": ["public"]
        }
      }
    }
  }
}

Step 2: Load the SDK in your app

In your main component or game screen, add the following:

import React, { useEffect, useState } from 'react';
import { View, Text, Button, Platform } from 'react-native';

export default function GameScreen() {
  const [tracker, setTracker] = useState(null);
  const [sessionData, setSessionData] = useState(null);
  
  useEffect(() => {
    // Only run on web platform
    if (Platform.OS === 'web') {
      // Load the SDK script
      const script = document.createElement('script');
      script.src = '/game-session-tracker.js';
      script.async = true;
      script.onload = () => initializeTracker();
      document.body.appendChild(script);
      
      // Parse URL parameters
      const urlParams = new URLSearchParams(window.location.search);
      setSessionData({
        token: urlParams.get('token') || generateRandomToken(),
        appVersion: urlParams.get('appVersion') || '1.0.0',
        language: urlParams.get('language') || 'en',
        sessionStartTime: new Date()
      });
    }
    
    return () => {
      // Cleanup script on unmount
      if (Platform.OS === 'web') {
        const script = document.querySelector('script[src="/game-session-tracker.js"]');
        if (script) document.body.removeChild(script);
      }
    };
  }, []);
  
  const initializeTracker = () => {
    if (window.GameSessionTracker) {
      // The SDK will automatically detect the subdomain
      setTracker(new window.GameSessionTracker());
    }
  };
  
  const generateRandomToken = () => {
    return 'player-' + Math.random().toString(36).substring(2, 15);
  };
  
  // Function to call when game ends
  const handleGameOver = (score) => {
    if (tracker) {
      tracker.endSession(score)
        .then(response => {
          console.log("Session recorded successfully", response);
        })
        .catch(error => {
          console.error("Failed to record session", error);
        });
    }
  };
  
  // Function to display leaderboard
  const showLeaderboard = () => {
    if (tracker) {
      tracker.getLeaderboard(10)
        .then(response => {
          const leaderboard = response.data.leaderboard;
          // Display leaderboard in your UI
          console.log("Leaderboard:", leaderboard);
        })
        .catch(error => {
          console.error("Failed to fetch leaderboard", error);
        });
    }
  };
  
  return (
    <View style="{ flex: 1, justifyContent: 'center', alignItems: 'center' }">
      {/* Your game UI */}
      <Button title="End Game" onPress={() => handleGameOver(1500)} />
      <Button title="Show Leaderboard" onPress={showLeaderboard} />
    </View>
  );
}

Option 2: Custom Implementation

If you prefer to implement session tracking yourself, follow these steps:

1. Create a session tracking service:

// services/SessionTracker.js
export default class SessionTracker {
  constructor() {
    this.token = null;
    this.appVersion = null;
    this.language = null;
    this.sessionStartTime = null;
    this.initialized = false;
    
    // Initialize on creation
    this.initialize();
  }
  
  initialize() {
    if (typeof window === 'undefined') return; // Skip on server-side
    
    const urlParams = new URLSearchParams(window.location.search);
    this.token = urlParams.get('token') || this.generateRandomToken();
    this.appVersion = urlParams.get('appVersion') || '1.0.0';
    this.language = urlParams.get('language') || 'en';
    this.sessionStartTime = new Date();
    this.initialized = true;
  }
  
  generateRandomToken() {
    return 'player-' + Math.random().toString(36).substring(2, 15);
  }
  
  async endSession(score) {
    if (!this.initialized) {
      console.error('Session tracker not initialized');
      return;
    }
    
    const sessionEndTime = new Date();
    
    const payload = {
      token: this.token,
      session_start_time: this.sessionStartTime.toISOString(),
      session_end_time: sessionEndTime.toISOString(),
      score: score,
      app_version: this.appVersion,
      language: this.language
    };
    
    // Get the subdomain from the current hostname
    const hostname = window.location.hostname;
    const parts = hostname.split('.');
    const subdomain = parts.length > 2 ? parts[0] : '';
    
    // REQUIRED: This exact endpoint must be called when a game session ends
    try {
      const response = await fetch(`/api/v1/games/${subdomain}/session-ended`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(payload)
      });
      
      return await response.json();
    } catch (error) {
      console.error('Failed to record session', error);
      throw error;
    }
  }
  
  async getLeaderboard(limit = 10) {
    // Get the subdomain from the current hostname
    const hostname = window.location.hostname;
    const parts = hostname.split('.');
    const subdomain = parts.length > 2 ? parts[0] : '';
    
    try {
      const response = await fetch(`/api/v1/games/${subdomain}/leaderboard?limit=${limit}`);
      return await response.json();
    } catch (error) {
      console.error('Failed to fetch leaderboard', error);
      throw error;
    }
  }
}

2. Use the service in your game component:

// screens/GameScreen.js
import React, { useEffect, useState } from 'react';
import { View, Button, Platform } from 'react-native';
import SessionTracker from '../services/SessionTracker';

export default function GameScreen() {
  const [tracker, setTracker] = useState(null);
  
  useEffect(() => {
    // Initialize the tracker on web platform only
    if (Platform.OS === 'web') {
      // The tracker will automatically detect the subdomain
      setTracker(new SessionTracker());
    }
  }, []);
  
  const handleGameOver = async (score) => {
    if (tracker) {
      try {
        const result = await tracker.endSession(score);
        console.log('Session recorded successfully', result);
      } catch (error) {
        console.error('Error recording session', error);
      }
    }
  };
  
  return (
    <View style="{ flex: 1, justifyContent: 'center', alignItems: 'center' }">
      {/* Your game UI */}
      <Button title="End Game" onPress={() => handleGameOver(1500)} />
    </View>
  );
}

Testing Your Implementation

Before uploading, test your session tracking implementation:

  1. Run your Expo web app with yarn web or expo start --web
  2. Add URL parameters to your local development URL (e.g., http://localhost:19006/?token=test123&appVersion=1.0.0&language=en)
  3. Use browser developer tools to verify network requests are being made correctly

In browser developer tools, check that the POST request to /api/v1/games/:gameId/session-ended is being made with all required parameters.

Uploading to Jiran Games

Log in to Your Account

Sign in to your Jiran Games developer account.

Create a New Game

Click on "Add New Game" and fill in the required information:

  • Game Name
  • Description
  • Game Logo (recommended size: 512x512px)
  • Tags (to help users find your game)
  • Subdomain (this will be your game's URL: yourgame.jiran.games)

Upload Your Game Bundle

Select the ZIP file containing your Expo web build and upload it.

Note: Our system will automatically detect your Expo web build structure and serve it correctly. We specifically look for the _expo folder, assets folder, and required HTML files.

Submit for Review

After uploading, your game will be marked as "pending_review". Our team will review it to ensure it meets our guidelines.

The review process typically takes 1-2 business days.

Alternative: Using Expo's dist Folder

Some newer versions of Expo may output to a dist folder instead of web-build. Our platform supports this structure as well.

Build with Newer Expo Versions

If you're using a newer version of Expo, the build command might create a dist folder instead:

# Build for web with newer Expo versions
npx expo export:web

This will create a dist folder with the following structure:

dist/
  ├── _expo/
  ├── assets/
  ├── index.html
  ├── not-found.html
  ├── _sitemap.html
  └── ...

Zip the dist Folder

Compress your dist directory into a ZIP file:

# On macOS/Linux
cd dist
zip -r ../my-game.zip ./*

# On Windows
# Right-click the dist folder > Send to > Compressed (zipped) folder

Note: Our platform will automatically detect both the web-build and dist folder structures for Expo projects.