├── .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 | }
--------------------------------------------------------------------------------