Implement React example of WS integration with Flipper
Summary: Create an example of how one can use `js-flipper` in a browser to connect to Flipper over WS. Reviewed By: mweststrate Differential Revision: D31688114 fbshipit-source-id: 135f826daeddeda8dca5b3df6504cc2bdc04dd1b
This commit is contained in:
committed by
Facebook GitHub Bot
parent
25a6fc1ab1
commit
02115722b3
@@ -7,6 +7,7 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Borrowed from https://github.com/strong-roots-capital/websocket-close-codes
|
||||||
export enum WSCloseCode {
|
export enum WSCloseCode {
|
||||||
/**
|
/**
|
||||||
* Normal closure; the connection successfully completed whatever
|
* Normal closure; the connection successfully completed whatever
|
||||||
|
|||||||
@@ -23,11 +23,21 @@ parameter, that registers a client plugin and will fire the relevant callbacks
|
|||||||
if the corresponding desktop plugin is selected in the Flipper Desktop. The full
|
if the corresponding desktop plugin is selected in the Flipper Desktop. The full
|
||||||
plugin API is documented
|
plugin API is documented
|
||||||
[here](https://fbflipper.com/docs/extending/create-plugin).
|
[here](https://fbflipper.com/docs/extending/create-plugin).
|
||||||
- `start` method. It starts the client.
|
- `start` method. It starts the client. It has two arguments:
|
||||||
- `appName` setter to set the app name displayed in Flipper
|
- `appName` - (required) the name dsplayed in Flipper
|
||||||
- `onError` setter to override how errors are handled (it is simple `console.error` by default)
|
- `options` which conforms to the infterface
|
||||||
- `websocketFactory` setter to override WebSocket implementation (Node.js folks, it is for you!)
|
```ts
|
||||||
- `urlBase` setter to make the client connect to a different URL
|
interface FlipperClientOptions {
|
||||||
|
// Make the client connect to a different URL
|
||||||
|
urlBase?: string;
|
||||||
|
// Override WebSocket implementation (Node.js folks, it is for you!)
|
||||||
|
websocketFactory?: (url: string) => FlipperWebSocket;
|
||||||
|
// Override how errors are handled (it is simple `console.error` by default)
|
||||||
|
onError?: (e: unknown) => void;
|
||||||
|
// Timeout after which client tries to reconnect to Flipper
|
||||||
|
reconnectTimeout?: number;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Example (web)
|
## Example (web)
|
||||||
|
|
||||||
@@ -49,18 +59,15 @@ interface of the
|
|||||||
[web version](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket).
|
[web version](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket).
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import {setWebSocketImplementation, start} from 'js-flipper';
|
import flipperClient from 'js-flipper';
|
||||||
// Say, you decided to go with 'ws'
|
// Say, you decided to go with 'ws'
|
||||||
// https://github.com/websockets/ws
|
// https://github.com/websockets/ws
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
|
|
||||||
// You need to let the flipper client know about it.
|
// Start the client and pass some options
|
||||||
setWebSocketImplementation(url => new WebSocket(url, {origin: 'localhost:'}));
|
|
||||||
// You might ask yourself why there is the second argument `{ origin: 'localhost:' }`
|
// You might ask yourself why there is the second argument `{ origin: 'localhost:' }`
|
||||||
// Flipper Desktop verifies the `Origin` header for every WS connection. You need to set it to one of the whitelisted values (see `VALID_WEB_SOCKET_REQUEST_ORIGIN_PREFIXES`).
|
// Flipper Desktop verifies the `Origin` header for every WS connection. You need to set it to one of the whitelisted values (see `VALID_WEB_SOCKET_REQUEST_ORIGIN_PREFIXES`).
|
||||||
|
flipperClient.start('My cool nodejs app', { websocketFactory: url => new WebSocket(url, {origin: 'localhost:'}) });
|
||||||
// Now, the last bit. You need to start the client.
|
|
||||||
start();
|
|
||||||
```
|
```
|
||||||
|
|
||||||
An example plugin should be somewhat similar to
|
An example plugin should be somewhat similar to
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {assert, detectDevice, detectOS} from './util';
|
|||||||
import {RECONNECT_TIMEOUT} from './consts';
|
import {RECONNECT_TIMEOUT} from './consts';
|
||||||
|
|
||||||
// TODO: Share with flipper-server-core
|
// TODO: Share with flipper-server-core
|
||||||
|
// Borrowed from https://github.com/strong-roots-capital/websocket-close-codes
|
||||||
/**
|
/**
|
||||||
* IANA WebSocket close code definitions.
|
* IANA WebSocket close code definitions.
|
||||||
*
|
*
|
||||||
@@ -123,25 +124,33 @@ export interface FlipperWebSocket {
|
|||||||
readyState: number;
|
readyState: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FlipperClientOptions {
|
||||||
|
// Make the client connect to a different URL
|
||||||
|
urlBase?: string;
|
||||||
|
// Override WebSocket implementation (Node.js folks, it is for you!)
|
||||||
|
websocketFactory?: (url: string) => FlipperWebSocket;
|
||||||
|
// Override how errors are handled (it is simple `console.error` by default)
|
||||||
|
onError?: (e: unknown) => void;
|
||||||
|
// Timeout after which client tries to reconnect to Flipper
|
||||||
|
reconnectTimeout?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class FlipperClient {
|
export class FlipperClient {
|
||||||
protected plugins: Map<string, FlipperPlugin> = new Map();
|
private readonly plugins: Map<string, FlipperPlugin> = new Map();
|
||||||
protected connections: Map<string, FlipperConnection> = new Map();
|
private readonly connections: Map<string, FlipperConnection> = new Map();
|
||||||
private ws?: FlipperWebSocket;
|
private ws?: FlipperWebSocket;
|
||||||
private devicePseudoId = `${Date.now()}.${Math.random()}`;
|
private readonly devicePseudoId = `${Date.now()}.${Math.random()}`;
|
||||||
private os = detectOS();
|
private readonly os = detectOS();
|
||||||
private device = detectDevice();
|
private readonly device = detectDevice();
|
||||||
private _appName = 'JS App';
|
|
||||||
private reconnectionTimer?: NodeJS.Timeout;
|
private reconnectionTimer?: NodeJS.Timeout;
|
||||||
private resolveStartPromise?: () => void;
|
private resolveStartPromise?: () => void;
|
||||||
|
private urlBase!: string;
|
||||||
|
private websocketFactory!: (url: string) => FlipperWebSocket;
|
||||||
|
private onError!: (e: unknown) => void;
|
||||||
|
private reconnectTimeout!: number;
|
||||||
|
private appName!: string;
|
||||||
|
|
||||||
public urlBase = `localhost:8333`;
|
constructor() {}
|
||||||
|
|
||||||
public websocketFactory: (url: string) => FlipperWebSocket = (url) =>
|
|
||||||
new WebSocket(url) as FlipperWebSocket;
|
|
||||||
public onError: (e: unknown) => void = (e: unknown) =>
|
|
||||||
console.error('WebSocket error', e);
|
|
||||||
|
|
||||||
constructor(public readonly reconnectTimeout = RECONNECT_TIMEOUT) {}
|
|
||||||
|
|
||||||
addPlugin(plugin: FlipperPlugin) {
|
addPlugin(plugin: FlipperPlugin) {
|
||||||
this.plugins.set(plugin.getId(), plugin);
|
this.plugins.set(plugin.getId(), plugin);
|
||||||
@@ -155,11 +164,25 @@ export class FlipperClient {
|
|||||||
return this.plugins.get(id);
|
return this.plugins.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(
|
||||||
|
appName: string,
|
||||||
|
{
|
||||||
|
urlBase = 'localhost:8333',
|
||||||
|
websocketFactory = (url) => new WebSocket(url) as FlipperWebSocket,
|
||||||
|
onError = (e) => console.error('WebSocket error', e),
|
||||||
|
reconnectTimeout = RECONNECT_TIMEOUT,
|
||||||
|
}: FlipperClientOptions = {},
|
||||||
|
): Promise<void> {
|
||||||
if (this.ws) {
|
if (this.ws) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.appName = appName;
|
||||||
|
this.onError = onError;
|
||||||
|
this.urlBase = urlBase;
|
||||||
|
this.websocketFactory = websocketFactory;
|
||||||
|
this.reconnectTimeout = reconnectTimeout;
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
return new Promise<void>((resolve) => {
|
||||||
this.resolveStartPromise = resolve;
|
this.resolveStartPromise = resolve;
|
||||||
this.connectToFlipper();
|
this.connectToFlipper();
|
||||||
@@ -189,17 +212,6 @@ export class FlipperClient {
|
|||||||
return !!this.ws && this.ws.readyState === 1;
|
return !!this.ws && this.ws.readyState === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
get appName() {
|
|
||||||
return this._appName;
|
|
||||||
}
|
|
||||||
|
|
||||||
set appName(newAppName: string) {
|
|
||||||
this._appName = newAppName;
|
|
||||||
|
|
||||||
this.ws?.close(WSCloseCode.NormalClosure);
|
|
||||||
this.reconnect(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private connectToFlipper() {
|
private connectToFlipper() {
|
||||||
const url = `ws://${this.urlBase}?device_id=${this.device}${this.devicePseudoId}&device=${this.device}&app=${this.appName}&os=${this.os}`;
|
const url = `ws://${this.urlBase}?device_id=${this.device}${this.devicePseudoId}&device=${this.device}&app=${this.appName}&os=${this.os}`;
|
||||||
|
|
||||||
@@ -211,7 +223,7 @@ export class FlipperClient {
|
|||||||
this.ws.onclose = ({code}) => {
|
this.ws.onclose = ({code}) => {
|
||||||
// Some WS implementations do not properly set `wasClean`
|
// Some WS implementations do not properly set `wasClean`
|
||||||
if (code !== WSCloseCode.NormalClosure) {
|
if (code !== WSCloseCode.NormalClosure) {
|
||||||
this.reconnect(false);
|
this.reconnect();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -235,7 +247,7 @@ export class FlipperClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Reconnect in a loop with an exponential backoff
|
// TODO: Reconnect in a loop with an exponential backoff
|
||||||
private reconnect(now?: boolean) {
|
private reconnect() {
|
||||||
this.ws = undefined;
|
this.ws = undefined;
|
||||||
|
|
||||||
if (this.reconnectionTimer) {
|
if (this.reconnectionTimer) {
|
||||||
@@ -243,12 +255,9 @@ export class FlipperClient {
|
|||||||
this.reconnectionTimer = undefined;
|
this.reconnectionTimer = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.reconnectionTimer = setTimeout(
|
this.reconnectionTimer = setTimeout(() => {
|
||||||
() => {
|
this.connectToFlipper();
|
||||||
this.connectToFlipper();
|
}, this.reconnectTimeout);
|
||||||
},
|
|
||||||
now ? 0 : this.reconnectTimeout,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMessageReceived(message: {
|
private onMessageReceived(message: {
|
||||||
|
|||||||
@@ -7,10 +7,9 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {FlipperClient, FlipperWebSocket, WSCloseCode} from './client';
|
import {FlipperClient} from './client';
|
||||||
import {FlipperPlugin} from './plugin';
|
|
||||||
|
|
||||||
|
export * from './client';
|
||||||
export * from './plugin';
|
export * from './plugin';
|
||||||
export {FlipperWebSocket};
|
|
||||||
|
|
||||||
export const flipperClient = new FlipperClient();
|
export const flipperClient = new FlipperClient();
|
||||||
|
|||||||
23
js/react-flipper-example/.gitignore
vendored
Normal file
23
js/react-flipper-example/.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
21
js/react-flipper-example/LICENSE
Normal file
21
js/react-flipper-example/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
9
js/react-flipper-example/README.md
Normal file
9
js/react-flipper-example/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# React Flipper Example
|
||||||
|
|
||||||
|
Examplary integration of any browser app with Flipper.
|
||||||
|
|
||||||
|
## How to start
|
||||||
|
|
||||||
|
1. `yarn` to install the deps
|
||||||
|
2. `yarn relative-deps` to link `js-flipper`
|
||||||
|
3. `yarn start` to start the app
|
||||||
50
js/react-flipper-example/package.json
Normal file
50
js/react-flipper-example/package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "react-flipper-example",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
|
"@testing-library/react": "^11.1.0",
|
||||||
|
"@testing-library/user-event": "^12.1.10",
|
||||||
|
"@types/jest": "^26.0.15",
|
||||||
|
"@types/node": "^12.0.0",
|
||||||
|
"@types/react": "^17.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2",
|
||||||
|
"react-scripts": "4.0.3",
|
||||||
|
"typescript": "^4.1.2",
|
||||||
|
"web-vitals": "^1.0.1"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject",
|
||||||
|
"prepare": "relative-deps"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": [
|
||||||
|
"react-app",
|
||||||
|
"react-app/jest"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"relative-deps": "^1.0.7"
|
||||||
|
},
|
||||||
|
"relativeDependencies": {
|
||||||
|
"js-flipper": "../js-flipper"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
js/react-flipper-example/public/index.html
Normal file
22
js/react-flipper-example/public/index.html
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>React App</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
js/react-flipper-example/src/App.tsx
Normal file
27
js/react-flipper-example/src/App.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {FC} from 'react';
|
||||||
|
import FlipperTicTacToe from './FlipperTicTacToe';
|
||||||
|
|
||||||
|
|
||||||
|
const App: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header>
|
||||||
|
React Flipper WebSocket Example
|
||||||
|
</header>
|
||||||
|
<section>
|
||||||
|
<FlipperTicTacToe />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
27
js/react-flipper-example/src/FlipperTicTacToe.css
Normal file
27
js/react-flipper-example/src/FlipperTicTacToe.css
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: repeat(3, 40px);
|
||||||
|
grid-template-columns: repeat(3, 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell {
|
||||||
|
line-height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:disabled {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell:hover:not(:disabled), .cell:focus:not(:disabled) {
|
||||||
|
background-color: blue;
|
||||||
|
}
|
||||||
93
js/react-flipper-example/src/FlipperTicTacToe.tsx
Normal file
93
js/react-flipper-example/src/FlipperTicTacToe.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {useState, useEffect, FC} from 'react';
|
||||||
|
import type {FlipperPluginConnection, FlipperClient} from 'js-flipper';
|
||||||
|
import './FlipperTicTacToe.css';
|
||||||
|
|
||||||
|
// We want to import and start flipper client only in development and test modes
|
||||||
|
let flipperClientPromise: Promise<FlipperClient> | undefined;
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
flipperClientPromise = import('js-flipper').then(({flipperClient}) => {
|
||||||
|
flipperClient.start('React Tic-Tac-Toe');
|
||||||
|
return flipperClient;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GameState {
|
||||||
|
cells: string[];
|
||||||
|
turn: string;
|
||||||
|
winner: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FlipperTicTacToe: FC = () => {
|
||||||
|
const [status, setStatus] = useState('Waiting for Flipper Desktop Player...');
|
||||||
|
const [gameState, setGameState] = useState<GameState>({
|
||||||
|
cells: [],
|
||||||
|
turn: ' ',
|
||||||
|
winner: ' ',
|
||||||
|
});
|
||||||
|
const [connection, setConnection] = useState<FlipperPluginConnection>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
flipperClientPromise?.then(flipperClient => {
|
||||||
|
flipperClient.addPlugin({
|
||||||
|
getId() {
|
||||||
|
return 'ReactNativeTicTacToe';
|
||||||
|
},
|
||||||
|
onConnect(connection) {
|
||||||
|
setStatus('Desktop player present');
|
||||||
|
setConnection(connection);
|
||||||
|
|
||||||
|
// listen to updates
|
||||||
|
connection.receive('SetState', (gameState: GameState) => {
|
||||||
|
if (gameState.winner !== ' ') {
|
||||||
|
setStatus(
|
||||||
|
`Winner is ${gameState.winner}! Waiting for a new game...`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setStatus(
|
||||||
|
gameState.turn === 'X'
|
||||||
|
? 'Your turn...'
|
||||||
|
: 'Awaiting desktop players turn...',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setGameState(gameState);
|
||||||
|
});
|
||||||
|
|
||||||
|
// request initial state
|
||||||
|
connection.send('GetState');
|
||||||
|
},
|
||||||
|
onDisconnect() {
|
||||||
|
setConnection(undefined);
|
||||||
|
setStatus('Desktop player gone...');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>{status}</p>
|
||||||
|
<div className="grid">
|
||||||
|
{gameState.cells.map((state, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
disabled={!connection || gameState.turn !== 'X' || state !== ' '}
|
||||||
|
onClick={() => connection?.send('XMove', {move: idx})}
|
||||||
|
className="cell">
|
||||||
|
{state}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FlipperTicTacToe;
|
||||||
20
js/react-flipper-example/src/index.tsx
Normal file
20
js/react-flipper-example/src/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
||||||
10
js/react-flipper-example/src/react-app-env.d.ts
vendored
Normal file
10
js/react-flipper-example/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||||
|
*
|
||||||
|
* This source code is licensed under the MIT license found in the
|
||||||
|
* LICENSE file in the root directory of this source tree.
|
||||||
|
*
|
||||||
|
* @format
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="react-scripts" />
|
||||||
27
js/react-flipper-example/tsconfig.json
Normal file
27
js/react-flipper-example/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src"
|
||||||
|
]
|
||||||
|
}
|
||||||
11654
js/react-flipper-example/yarn.lock
Normal file
11654
js/react-flipper-example/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user