├── .gitignore ├── .idea ├── .gitignore ├── Example-React-Native.iml ├── modules.xml └── vcs.xml ├── App.tsx ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── app.json ├── assets ├── adaptive-icon.png ├── favicon.png ├── icon.png └── splash.png ├── babel.config.js ├── package-lock.json ├── package.json ├── src ├── events │ ├── asset-unlocked.event.ts │ ├── avatar-exported.event.ts │ ├── index.ts │ ├── user-authorized.event.ts │ ├── user-logged-out.event.ts │ ├── user-set.event.ts │ └── user-updated.event.ts ├── hooks │ ├── use-2d-image-url.ts │ └── use-avatar-creator-url.ts ├── index.ts ├── pages │ └── avatar.tsx └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .env.local 4 | .env.development.local 5 | .env.test.local 6 | .env.production.local 7 | .expo 8 | node_modules -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | -------------------------------------------------------------------------------- /.idea/Example-React-Native.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react'; 2 | import WebView, { WebViewMessageEvent } from 'react-native-webview'; 3 | import { useAvatarCreatorUrl } from './src/hooks/use-avatar-creator-url'; 4 | import { 5 | AssetUnlockedEvent, 6 | AvatarCreatorEvent, 7 | AvatarExportedEvent, 8 | UserAuthorizedEvent, 9 | UserSetEvent, 10 | UserUpdatedEvent, 11 | UserLoggedOutEvent 12 | } from './src'; 13 | import { Alert } from 'react-native'; 14 | import AvatarPage from './src/pages/avatar'; 15 | 16 | const RPM_TARGET = 'readyplayerme'; 17 | 18 | // Replace with your custom subdomain 19 | const subdomain = 'demo'; 20 | 21 | export default function App() { 22 | const webView = useRef(); 23 | const url = useAvatarCreatorUrl(subdomain, {}); 24 | 25 | const [avatarId, setAvatarId] = useState(); 26 | 27 | const supportedEvents = { 28 | 'v1.avatar.exported': onAvatarExported, 29 | 'v1.user.set': onUserSet, 30 | 'v1.user.authorized': onUserAuthorized, 31 | 'v1.asset.unlock': onAssetUnlocked, 32 | 'v1.user.updated': onUserUpdated, 33 | 'v1.user.logout': onUserLoggedOut 34 | } as Record; 35 | 36 | function onAvatarExported(message: AvatarExportedEvent) { 37 | setAvatarId(message.data.avatarId); 38 | } 39 | 40 | function onAssetUnlocked(message: AssetUnlockedEvent) { 41 | Alert.alert(`Asset Unlocked | Asset ID = ${message.data?.assetId}`); 42 | } 43 | 44 | function onUserAuthorized(message: UserAuthorizedEvent) { 45 | Alert.alert(`User Authorized | User ID = ${message.data?.id}`); 46 | } 47 | 48 | function onUserSet(message: UserSetEvent) { 49 | Alert.alert(`User Set | User ID = ${message.data?.id}`); 50 | } 51 | 52 | function onUserUpdated(message: UserUpdatedEvent) { 53 | Alert.alert(`User Updated | User ID = ${message.data?.id}`); 54 | } 55 | 56 | function onUserLoggedOut(message: UserLoggedOutEvent) { 57 | Alert.alert(`User Logged Out`); 58 | } 59 | 60 | function onWebViewLoaded() { 61 | webView.current?.postMessage( 62 | JSON.stringify({ 63 | target: 'readyplayerme', 64 | type: 'subscribe', 65 | eventName: 'v1.**' 66 | }) 67 | ); 68 | } 69 | 70 | function onMessageReceived(message: WebViewMessageEvent) { 71 | const data = message.nativeEvent.data; 72 | const event = JSON.parse(data) as AvatarCreatorEvent; 73 | 74 | if (event?.source !== RPM_TARGET || !event.eventName) { 75 | return; 76 | } 77 | 78 | supportedEvents[event.eventName]?.(event); 79 | } 80 | 81 | if (avatarId) { 82 | return setAvatarId('')} avatarId={avatarId}>; 83 | } 84 | 85 | return ( 86 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [0.3.0] - 2023.03.27 7 | 8 | ## Changed 9 | 10 | - Package versions are updated. 11 | 12 | ## Removed 13 | 14 | - Unused packages are removed. 15 | - Subscription deleted event removed. 16 | 17 | ## [0.2.0] - 2022.03.31 18 | 19 | ## Added 20 | 21 | - support for new post message system 22 | 23 | ## Removed 24 | 25 | - legacy post message event listening 26 | 27 | ## [0.1.0] - 2021-06-16 28 | 29 | ### Added 30 | 31 | - Initial React Native example 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ready Player Me 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ready Player Me embedded in a React Native WebView 2 | 3 | This example showcases embedding [Ready Player Me](https://readyplayer.me) avatar creator as a `WebView` to React Native mobile applications and retrieving the URL to the 3D model of the avatar. 4 | 5 | The example uses the cross-platform (iOS, Android, MacOS, Windows) [react-native-webview](https://github.com/react-native-webview/react-native-webview) module to load and interact with Ready Player Me inside a WebView. 6 | 7 | ## Demo 8 | 9 | Try this example [live on Snack](https://snack.expo.io/@rainerwolf3d/readyplayerme-with-react-native-expo). 10 | 11 | ## How to use 12 | 13 | This example uses [expo.io](https://expo.io) for bootstrapping the React Native project, however it should be easily adaptable for any React Native project using the [react-native-webview](https://github.com/react-native-webview/react-native-webview) package. 14 | 15 | 1. Install [expo-cli](https://docs.expo.io/get-started/installation/). 16 | 17 | ``` 18 | npm install --global expo-cli 19 | ``` 20 | 21 | 2. Run the project in development mode 22 | 23 | ``` 24 | npm install 25 | expo start 26 | ``` 27 | 28 | --- 29 | 30 | ## About Ready Player Me 31 | 32 | [Ready Player Me](https://readyplayer.me/developers) is a cross-game avatar platform for Unity, Unreal Engine, and all web-based stacks. 33 | 34 | We give users an interoperable personal avatar that travels with them across many virtual experiences. Developers use our plug-and-play avatar creator so they wouldn’t have to spend months on building their own. 35 | 36 | 37 | ### Getting Started 38 | 39 | Visit [docs.readyplayer.me](https://docs.readyplayer.me/) to get started with integrating Ready Player Me or explore the [examples](https://github.com/readyplayerme). 40 | 41 | ### Community 42 | 43 | Get in touch with the Ready Player Me community over at our [Forums](https://forum.readyplayer.me/). 44 | 45 | ### Becoming a partner 46 | 47 | Ready Player Me is already used in [thousands of apps and games](https://readyplayer.me/partners). 48 | 49 | Want to add Ready Player Me avatars to your own app or game? Apply [here](http://readyplayer.me/become-a-partner) or let us know at [support@readyplayer.me](mailto:support@readyplayer.me). 50 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "readyplayerme-with-react-native-expo", 4 | "slug": "snack-e7a93c7b-1d18-4d23-8ce0-2edf67fc903c", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": ["**/*"], 17 | "ios": { 18 | "supportsTablet": true 19 | }, 20 | "android": { 21 | "adaptiveIcon": { 22 | "foregroundImage": "./assets/adaptive-icon.png", 23 | "backgroundColor": "#FFFFFF" 24 | } 25 | }, 26 | "web": { 27 | "favicon": "./assets/favicon.png" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readyplayerme/Example-React-Native/34b3d56cb836d2cc600d22bfbecdcac702ad1c89/assets/adaptive-icon.png -------------------------------------------------------------------------------- /assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readyplayerme/Example-React-Native/34b3d56cb836d2cc600d22bfbecdcac702ad1c89/assets/favicon.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readyplayerme/Example-React-Native/34b3d56cb836d2cc600d22bfbecdcac702ad1c89/assets/icon.png -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/readyplayerme/Example-React-Native/34b3d56cb836d2cc600d22bfbecdcac702ad1c89/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web" 8 | }, 9 | "dependencies": { 10 | "expo": "^48.0.9", 11 | "react": "18.2.0", 12 | "react-native": "0.71.4", 13 | "react-native-webview": "11.26.0" 14 | }, 15 | "devDependencies": { 16 | "@babel/core": "^7.20.0", 17 | "@tsconfig/react-native": "^3.0.2", 18 | "@types/jest": "^29.5.5", 19 | "@types/react": "^18.2.27", 20 | "@types/react-native": "^0.72.3", 21 | "@types/react-test-renderer": "^18.0.3", 22 | "typescript": "^5.2.2" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /src/events/asset-unlocked.event.ts: -------------------------------------------------------------------------------- 1 | import { IFrameEvent } from '../types'; 2 | 3 | export type AssetUnlockedEventPayload = { 4 | userId: string; 5 | assetId: string; 6 | }; 7 | 8 | export type AssetUnlockedEvent = IFrameEvent; 9 | -------------------------------------------------------------------------------- /src/events/avatar-exported.event.ts: -------------------------------------------------------------------------------- 1 | import { IFrameEvent } from '../types'; 2 | 3 | export type AvatarExportedEventPayload = { 4 | url: string; 5 | avatarId: string; 6 | userId: string; 7 | }; 8 | 9 | export type AvatarExportedEvent = IFrameEvent; 10 | -------------------------------------------------------------------------------- /src/events/index.ts: -------------------------------------------------------------------------------- 1 | import { UserSetEvent } from './user-set.event'; 2 | import { AssetUnlockedEvent } from './asset-unlocked.event'; 3 | import { UserAuthorizedEvent } from './user-authorized.event'; 4 | import { AvatarExportedEvent } from './avatar-exported.event'; 5 | import { UserUpdatedEvent } from './user-updated.event'; 6 | import { UserLoggedOutEvent } from './user-logged-out.event'; 7 | 8 | export type { AssetUnlockedEvent, AssetUnlockedEventPayload } from './asset-unlocked.event'; 9 | export type { AvatarExportedEvent, AvatarExportedEventPayload } from './avatar-exported.event'; 10 | export type { UserAuthorizedEvent, UserAuthorizedEventPayload } from './user-authorized.event'; 11 | export type { UserSetEvent, UserSetEventPayload } from './user-set.event'; 12 | export type { UserUpdatedEvent, UserUpdatedEventPayload } from './user-updated.event'; 13 | export type { UserLoggedOutEvent } from './user-logged-out.event'; 14 | 15 | export type AvatarCreatorEvent = 16 | | UserSetEvent 17 | | AssetUnlockedEvent 18 | | UserAuthorizedEvent 19 | | AvatarExportedEvent 20 | | UserUpdatedEvent 21 | | UserLoggedOutEvent; 22 | -------------------------------------------------------------------------------- /src/events/user-authorized.event.ts: -------------------------------------------------------------------------------- 1 | import { IFrameEvent } from '../types'; 2 | 3 | export type UserAuthorizedEventPayload = { 4 | id: string; 5 | }; 6 | 7 | export type UserAuthorizedEvent = IFrameEvent; 8 | -------------------------------------------------------------------------------- /src/events/user-logged-out.event.ts: -------------------------------------------------------------------------------- 1 | import { IFrameEvent } from '../types'; 2 | 3 | export type UserLoggedOutEvent = IFrameEvent; 4 | -------------------------------------------------------------------------------- /src/events/user-set.event.ts: -------------------------------------------------------------------------------- 1 | import { IFrameEvent } from '../types'; 2 | 3 | export type UserSetEventPayload = { 4 | id: string; 5 | }; 6 | 7 | export type UserSetEvent = IFrameEvent; 8 | -------------------------------------------------------------------------------- /src/events/user-updated.event.ts: -------------------------------------------------------------------------------- 1 | import { IFrameEvent } from '../types'; 2 | 3 | export type UserUpdatedEventPayload = { 4 | id: string; 5 | }; 6 | 7 | export type UserUpdatedEvent = IFrameEvent; 8 | -------------------------------------------------------------------------------- /src/hooks/use-2d-image-url.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo } from 'react'; 2 | import { Avatar2DConfig } from '../types'; 3 | 4 | export const use2dImageUrl = (modelId: string, config?: Avatar2DConfig) => { 5 | return useMemo(() => { 6 | let url = `https://models.readyplayer.me/${modelId}.png`; 7 | 8 | if (!config || Object.keys(config).length == 0) { 9 | return url.toString(); 10 | } 11 | 12 | var params = new URLSearchParams(config).toString(); 13 | 14 | return `${url}?${params}`; 15 | }, [modelId, config]); 16 | }; 17 | -------------------------------------------------------------------------------- /src/hooks/use-avatar-creator-url.ts: -------------------------------------------------------------------------------- 1 | import { AvatarCreatorConfig } from '../types'; 2 | import { useMemo } from 'react'; 3 | 4 | export const useAvatarCreatorUrl = (subdomain: string, config: AvatarCreatorConfig | undefined): string => { 5 | return useMemo(() => { 6 | let url = `https://${subdomain || `demo`}.readyplayer.me`; 7 | 8 | if (config?.language) url += `/${config.language}`; 9 | 10 | url += `/avatar?frameApi&source=react-native-avatar-creator`; 11 | 12 | if (config?.clearCache) url += '&clearCache'; 13 | 14 | if (config?.quickStart) url += '&quickStart'; 15 | 16 | if (config?.bodyType) url += `&bodyType=${config?.bodyType}`; 17 | 18 | return url; 19 | }, [subdomain, config]); 20 | }; 21 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { AvatarCreatorConfig, Language, BodyType, IFrameEvent } from './types'; 2 | export type * from './events'; 3 | -------------------------------------------------------------------------------- /src/pages/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { useState, type FC, useEffect } from 'react'; 2 | import { Button, Image, ImageStyle, StyleProp, StyleSheet, Text, View } from 'react-native'; 3 | import { use2dImageUrl } from '../hooks/use-2d-image-url'; 4 | import { Avatar2DConfig } from '../types'; 5 | 6 | interface AvatarPageProps { 7 | avatarId: string; 8 | clearAvatar: () => void; 9 | } 10 | 11 | const styles = StyleSheet.create({ 12 | container: { 13 | flex: 1, 14 | justifyContent: 'center', 15 | alignItems: 'center', 16 | gap: 20 17 | }, 18 | image: { 19 | width: 200, 20 | height: 200 21 | } 22 | }); 23 | 24 | const avatarConf: Avatar2DConfig = { 25 | expression: 'happy', 26 | pose: 'thumbs-up' 27 | }; 28 | 29 | const AvatarPage: FC = ({ avatarId, clearAvatar }) => { 30 | const url = use2dImageUrl(avatarId, avatarConf); 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | 40 | export default AvatarPage; 41 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type BodyType = 'halfbody' | 'fullbody'; 2 | 3 | export type Language = 4 | | 'en' 5 | | 'en-IE' 6 | | 'de' 7 | | 'fr' 8 | | 'es' 9 | | 'es-MX' 10 | | 'it' 11 | | 'pt' 12 | | 'pt-BR' 13 | | 'tr' 14 | | 'ja' 15 | | 'kr' 16 | | 'ch'; 17 | 18 | export type AvatarCreatorConfig = { 19 | clearCache?: boolean; 20 | bodyType?: BodyType; 21 | quickStart?: boolean; 22 | language?: Language; 23 | }; 24 | 25 | /** 26 | * These are some of the parameters to fetch custom 2D Avatar image 27 | * More info about rest of parameters is available here: 28 | * 29 | * https://docs.readyplayer.me/ready-player-me/api-reference/rest-api/avatars/get-2d-avatars 30 | */ 31 | export type Avatar2DConfig = { 32 | expression?: 'happy' | 'lol' | 'sad' | 'scared' | 'rage'; 33 | pose?: 'power-stance' | 'relaxed' | 'standing' | 'thumbs-up'; 34 | camera?: 'fullbody' | 'portrait'; 35 | }; 36 | 37 | export type IFrameEvent = { 38 | eventName?: string; 39 | source?: string; 40 | data: TPayload; 41 | }; 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/react-native/tsconfig.json" 3 | } --------------------------------------------------------------------------------