diff --git a/mobile/package.json b/mobile/package.json index 7205038..8cce6ef 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -13,11 +13,14 @@ "@react-navigation/native": "^6.1.17", "@react-navigation/native-stack": "^6.9.26", "i18next": "^23.10.1", + "mancala.js": "^0.0.2-beta.3", "react": "18.2.0", "react-i18next": "^14.1.0", "react-native": "0.73.6", + "react-native-mmkv": "^2.12.2", "react-native-safe-area-context": "^4.9.0", "react-native-screens": "^3.29.0", + "react-native-snackbar": "^2.6.2", "tiny-emitter": "^2.1.0" }, "devDependencies": { diff --git a/mobile/src/App.tsx b/mobile/src/App.tsx index 9863282..9750825 100644 --- a/mobile/src/App.tsx +++ b/mobile/src/App.tsx @@ -1,19 +1,80 @@ import * as React from 'react'; -import { NavigationContainer } from '@react-navigation/native'; +import { NavigationContainer, Theme } from '@react-navigation/native'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { RootStackParamList } from './types'; import { HomeScreen } from './screens/HomeScreen'; import LobyScreen from './screens/LobyScreen'; +import { initContext } from './context/context'; +import { GameScreen } from './screens/GameScreen'; +import { RTMTWS } from './rtmt/rtmt_websocket'; +import { ConnectionState } from './rtmt/rtmt'; +import { useTranslation } from 'react-i18next'; +import Snackbar from 'react-native-snackbar'; const Stack = createNativeStackNavigator(); +const context = initContext(); + function App() { + + const { t } = useTranslation(); + + const [userKey, setUserKey] = React.useState(undefined); + + const [connectionState, setConnetionState] = React.useState("connecting"); + + //@ts-ignore + const [theme, setTheme] = React.useState(context.themeManager.theme); + + const onConnectionError = (event: Event) => console.error("(RTMT) Connection Error: ", event); + + const onConnectionChange = (_connectionState: ConnectionState) => setConnetionState(_connectionState); + + const onThemeChange = (theme: Theme) => setTheme(theme); + + const connectRTMT = (userKey: string) => { + const rtmt = context.rtmt as RTMTWS; + rtmt.on("error", onConnectionError); + rtmt.on("connectionchange", onConnectionChange) + rtmt.connectWebSocket(userKey) + return rtmt; + } + + const loadUserKeyAndConnectServer = () => { + context.userKeyStore.getUserKey().then((userKey: string) => { + setUserKey(userKey); + connectRTMT(userKey); + }).catch((error) => { + //TODO: check if it is network error! + Snackbar.show({text: t("ErrorWhenRetrievingInformation")}) + console.error(error); + }); + } + + const disposeApp = () => { + context.rtmt?.dispose(); + context.themeManager.off("themechange", onThemeChange); + } + + React.useEffect(() => { + loadUserKeyAndConnectServer(); + context.themeManager.on("themechange", onThemeChange); + return () => disposeApp(); + }, []); + + //const textColorOnBoard = getColorByBrightness( + // context.themeManager.theme.boardColor, + // context.themeManager.theme.textColor, + // context.themeManager.theme.textLightColor + //); + return ( - + + ); diff --git a/mobile/src/context/context.tsx b/mobile/src/context/context.tsx new file mode 100644 index 0000000..3bca23e --- /dev/null +++ b/mobile/src/context/context.tsx @@ -0,0 +1,28 @@ +import { server } from "../const/config"; +import { RTMT } from "../rtmt/rtmt"; +import { RTMTWS } from "../rtmt/rtmt_websocket"; +import { HttpServiceImpl } from "../service/HttpService"; +import { GameStore, GameStoreImpl } from "../store/GameStore"; +import { UserKeyStore, UserKeyStoreImpl } from "../store/KeyStore"; +import ThemeManager from "../theme/ThemeManager"; + +export type Context = { + rtmt: RTMT; + userKeyStore: UserKeyStore; + themeManager: ThemeManager; + gameStore: GameStore; +}; + +export const initContext = (): Context => { + const rtmt = new RTMTWS(); + const httpService = new HttpServiceImpl(server.serverAdress); + const userKeyStore = new UserKeyStoreImpl({ httpService }); + const gameStore = new GameStoreImpl({ httpService }); + const themeManager = new ThemeManager(); + return { + rtmt: rtmt, + userKeyStore: userKeyStore, + themeManager, + gameStore + }; +}; diff --git a/mobile/src/localization/en.ts b/mobile/src/localization/en.ts index 090d986..f7c753b 100644 --- a/mobile/src/localization/en.ts +++ b/mobile/src/localization/en.ts @@ -1,6 +1,39 @@ export default { - translation: { - newGame: "New Game", - searchingOppenent: 'Please wait, searching opponent...', + translation: { + Mancala: "Mancala", + Leave: "Leave The Game", + NewGame: "New Game", + YourTurn: "Your Turn", + OpponentTurn: "Opponent Turn", + GameEnded: "Game Ended", + InternalErrorOccurred: "An internal error has occurred", + YouWon: "You Won", + Won: "Won", + YouLost: "You Lost", + Connecting: "Connecting", + Connected: "Connected", + CannotConnect: "Can't Connect", + ConnectionLost: "Network Connection Lost", + ConnectingAgain: "Connecting Again", + ServerError: "Server Error", + SearchingOpponet: "Searching Opponet", + OpponentLeftTheGame: "Opponent Leaves The Game", + YouLeftTheGame: "You Left The Game", + UserLeftTheGame: "Left The Game", + SearchingOpponent: "Searching Opponent", + PleaseWait: "Please Wait", + GameDraw: "Draw", + Anonymous: "Anonymous", + GameNotFound: "Game Not Found", + Loading: "Loading", + Playing: "Playing", + Error: "Error", + ErrorWhenRetrievingInformation: "An error occured when retrieving information!", + UCanOnlyPlayYourOwnPits: "You can only play your own pits", + UMustWaitUntilCurrentMoveComplete: "You must wait until the current move is complete", + UCanNotPlayEmptyPit: "You can not play empty pit", + AreYouSureToLeaveGame: "Are you sure to leave game?", + Yes: "Yes", + Cancel: "Cancel", } }; \ No newline at end of file diff --git a/mobile/src/localization/tr.ts b/mobile/src/localization/tr.ts index be382f8..256c8ae 100644 --- a/mobile/src/localization/tr.ts +++ b/mobile/src/localization/tr.ts @@ -1,6 +1,39 @@ export default { - translation: { - newGame: "Yeni Oyun", - searchingOppenent: 'Lütfen bekleyin, rakip bulunuyor...', + translation: { + Mancala: "Köçürme", + Leave: "Oyundan Ayrıl", + NewGame: "Yeni Oyun", + YourTurn: "Sıra Sende", + OpponentTurn: "Sıra Rakipte", + GameEnded: "Oyun Bitti", + InternalErrorOccurred: "Dahili bir hata oluştu", + YouWon: "Kazandın", + Won: "Kazandı", + YouLost: "Kaybettin", + Connecting: "Bağlanılıyor", + Connected: "Bağlandı", + CannotConnect: "Bağlanılamadı", + ConnectionLost: "Ağ Bağlantısı Koptu", + ConnectingAgain: "Tekrar Bağlanılıyor", + ServerError: "Sunucu Hatası", + SearchingOpponet: "Rakip Aranıyor", + OpponentLeftTheGame: "Rakip Oyundan Ayrıldı", + YouLeftTheGame: "Sen Oyundan Ayrıldın", + UserLeftTheGame: "Oyundan Ayrıldı", + SearchingOpponent: "Rakip Aranıyor", + PleaseWait: "Lütfen Bekleyin", + GameDraw: "Berabere", + Anonymous: "Anonim", + GameNotFound: "Oyun Bulunamadı", + Loading: "Yükleniyor", + Playing: "Oynuyor", + Error: "Hata", + ErrorWhenRetrievingInformation: "Bilgiler toplanırken bir hata oluştu!", + UCanOnlyPlayYourOwnPits: "Sadece sana ait olan kuyular ile oynayabilirsin", + UMustWaitUntilCurrentMoveComplete: "Devam eden haraketin bitmesini beklemelisin", + UCanNotPlayEmptyPit: "Boş kuyu ile oynayamazsın", + AreYouSureToLeaveGame: "Oyundan ayrılmak istediğine emin misin?", + Yes: "Evet", + Cancel: "İptal" } }; \ No newline at end of file diff --git a/mobile/src/models/Game.ts b/mobile/src/models/Game.ts new file mode 100644 index 0000000..ec37311 --- /dev/null +++ b/mobile/src/models/Game.ts @@ -0,0 +1,13 @@ +import { MancalaGame } from "mancala.js"; +import { UserConnectionInfo } from "./UserConnectionInfo"; + +export interface Game { + id: string; + mancalaGame: MancalaGame; + gameUsersConnectionInfo: GameUsersConnectionInfo; +} + +export interface GameUsersConnectionInfo { + user1ConnectionInfo: UserConnectionInfo; + user2ConnectionInfo: UserConnectionInfo; +} \ No newline at end of file diff --git a/mobile/src/models/GameMove.ts b/mobile/src/models/GameMove.ts new file mode 100644 index 0000000..dcd22f5 --- /dev/null +++ b/mobile/src/models/GameMove.ts @@ -0,0 +1,3 @@ +export interface GameMove { + index: number; +} diff --git a/mobile/src/models/LoadingState.tsx b/mobile/src/models/LoadingState.tsx new file mode 100644 index 0000000..dfe1e4e --- /dev/null +++ b/mobile/src/models/LoadingState.tsx @@ -0,0 +1,46 @@ +export type LoadingStateType = "unset" | "loading" | "loaded" | "error"; + +export class LoadingState { + state: LoadingStateType; + + errorMessage?: string; + + value?: T; + + constructor(props: { state?: LoadingStateType, errorMessage?: string, value?: T }) { + this.state = props.state ? props.state : "unset"; + this.errorMessage = props.errorMessage; + this.value = props.value; + } + + public static Unset() { + return new LoadingState({ state: "unset" }); + } + + public static Loading() { + return new LoadingState({ state: "loading" }); + } + + public static Error(props: { errorMessage: string }) { + const { errorMessage } = props; + return new LoadingState({ state: "error", errorMessage }); + } + + public static Loaded(props: { value?: T }) { + const { value } = props; + return new LoadingState({ state: "loaded", value }); + } + + public isUnset() : boolean { + return this.state === "unset"; + } + public isLoading() : boolean { + return this.state === "loading"; + } + public isError() : boolean { + return this.state === "error"; + } + public isLoaded() : boolean { + return this.state === "loaded"; + } +} \ No newline at end of file diff --git a/mobile/src/models/User.ts b/mobile/src/models/User.ts new file mode 100644 index 0000000..e5ab1ae --- /dev/null +++ b/mobile/src/models/User.ts @@ -0,0 +1,6 @@ +export interface User { + id: string; + name: string; + isOnline: boolean; + isAnonymous: boolean; +} \ No newline at end of file diff --git a/mobile/src/models/UserConnectionInfo.ts b/mobile/src/models/UserConnectionInfo.ts new file mode 100644 index 0000000..3ce426f --- /dev/null +++ b/mobile/src/models/UserConnectionInfo.ts @@ -0,0 +1,4 @@ +export interface UserConnectionInfo { + userId: string; + isOnline: boolean; +} \ No newline at end of file diff --git a/mobile/src/rtmt/rtmt_websocket.ts b/mobile/src/rtmt/rtmt_websocket.ts index 7ef29f2..c6648ee 100644 --- a/mobile/src/rtmt/rtmt_websocket.ts +++ b/mobile/src/rtmt/rtmt_websocket.ts @@ -109,7 +109,7 @@ export class RTMTWS extends TinyEmitter implements RTMT { this.emit(MESSAGE_CHANNEL_PREFIX + channel, message); } - public on(event: RtmtEventTypes, callback: (...value: any[]) => void): Listener | this { + public on(event: RtmtEventTypes, callback: (...value: any[]) => void): this { return super.on(event, callback); } diff --git a/mobile/src/screens/GameScreen.tsx b/mobile/src/screens/GameScreen.tsx index b8c1e6a..7a4b768 100644 --- a/mobile/src/screens/GameScreen.tsx +++ b/mobile/src/screens/GameScreen.tsx @@ -1,13 +1,16 @@ import * as React from 'react'; -import { View, Button } from 'react-native'; +import { View, Button, Text } from 'react-native'; import { useTranslation } from 'react-i18next'; import { GameScreenProps } from '../types'; export function GameScreen({ navigation, route }: GameScreenProps) { + + const { context, gameId } = route.params; const { t } = useTranslation(); return ( + {gameId} ); } diff --git a/mobile/src/screens/HomeScreen.tsx b/mobile/src/screens/HomeScreen.tsx index 487249d..e266ecb 100644 --- a/mobile/src/screens/HomeScreen.tsx +++ b/mobile/src/screens/HomeScreen.tsx @@ -4,13 +4,15 @@ import { useTranslation } from 'react-i18next'; import { HomeScreenProps } from '../types'; export function HomeScreen({ navigation, route }: HomeScreenProps) { + + const { context } = route.params; const { t } = useTranslation(); return ( + title={t('NewGame')} + onPress={() => navigation.navigate('Loby', { context })}> ); } diff --git a/mobile/src/screens/LobyScreen.tsx b/mobile/src/screens/LobyScreen.tsx index 687372c..432602c 100644 --- a/mobile/src/screens/LobyScreen.tsx +++ b/mobile/src/screens/LobyScreen.tsx @@ -2,13 +2,31 @@ import * as React from 'react'; import { View, Text, Button } from 'react-native'; import { useTranslation } from 'react-i18next'; import { LobyScreenProps } from '../types'; +import { useEffect } from 'react'; +import { CommonMancalaGame } from 'mancala.js'; +import { channel_on_game_start } from '../const/channel_names'; export default function LobyScreen({ navigation, route }: LobyScreenProps) { + + const { context } = route.params; const { t } = useTranslation(); + const onGameStart = (message: Object) => { + const newGame: CommonMancalaGame = message as CommonMancalaGame; + navigation.navigate("Game", { context, gameId: newGame.id }) + } + + useEffect(() => { + context.rtmt.addMessageListener(channel_on_game_start, onGameStart); + context.rtmt.sendMessage("new_game", {}); + return () => { + context.rtmt.removeMessageListener(channel_on_game_start, onGameStart); + } + }, []); + return ( - {t('searchingOppenent')} + {t('SearchingOpponent')} ); } \ No newline at end of file diff --git a/mobile/src/service/HttpService.ts b/mobile/src/service/HttpService.ts new file mode 100644 index 0000000..c07c2f7 --- /dev/null +++ b/mobile/src/service/HttpService.ts @@ -0,0 +1,23 @@ +import { server } from "../const/config"; + +export interface HttpService { + get: (route: string) => Promise; +} + +export class HttpServiceImpl implements HttpService { + public serverAdress: string; + + constructor(serverAdress: string) { + this.serverAdress = serverAdress; + } + + public async get(route: string): Promise { + const url = server.serverAdress + route; + const requestOptions = { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }; + const response = await fetch(url, requestOptions); + return response; + } +} diff --git a/mobile/src/storage/index.ts b/mobile/src/storage/index.ts new file mode 100644 index 0000000..a315458 --- /dev/null +++ b/mobile/src/storage/index.ts @@ -0,0 +1,3 @@ +import { MMKV } from 'react-native-mmkv' + +export const storage = new MMKV() \ No newline at end of file diff --git a/mobile/src/store/GameStore.ts b/mobile/src/store/GameStore.ts new file mode 100644 index 0000000..fc30033 --- /dev/null +++ b/mobile/src/store/GameStore.ts @@ -0,0 +1,28 @@ +import { CommonMancalaGame, MancalaGame } from "mancala.js"; +import { Game } from "../models/Game"; +import { HttpService } from "../service/HttpService"; + +export interface GameStore { + get(id: string): Promise; +} + +export class GameStoreImpl implements GameStore { + httpService: HttpService; + constructor(props: { httpService: HttpService }) { + this.httpService = props.httpService; + } + + async get(id: string): Promise { + try { + const response = await this.httpService.get(`/game/${id}`); + const json = await response.json(); + const game: Game = json as Game; + game.mancalaGame = MancalaGame.createFromMancalaGame(game.mancalaGame); + return game; + } catch (error) { + // todo check error + Promise.resolve(undefined); + } + } + +} \ No newline at end of file diff --git a/mobile/src/store/KeyStore.ts b/mobile/src/store/KeyStore.ts new file mode 100644 index 0000000..fa39c0c --- /dev/null +++ b/mobile/src/store/KeyStore.ts @@ -0,0 +1,54 @@ +import { HttpService } from "../service/HttpService" +import { storage } from "../storage"; + +const user_key = "user_key" + +export interface UserKeyStore { + getUserKey: () => Promise; +} + +export class UserKeyStoreImpl implements UserKeyStore { + private httpService: HttpService; + private keyStoreHttp: UserKeyStore; + private keyStoreLocalStorage = new UserKeyStoreLocalStorage() + + constructor(props: { httpService: HttpService }) { + this.httpService = props.httpService; + this.keyStoreHttp = new UserKeyStoreLocalHttp({ httpService: this.httpService }); + } + + public async getUserKey(): Promise { + const maybeUserKey = await this.keyStoreLocalStorage.getUserKey(); + if (maybeUserKey === undefined) { + const userKey = await this.keyStoreHttp.getUserKey() + this.keyStoreLocalStorage.storeUserKey(userKey) + return Promise.resolve(userKey); + } else { + return Promise.resolve(maybeUserKey); + } + } +} + +export class UserKeyStoreLocalHttp implements UserKeyStore { + httpService: HttpService; + + constructor(params: { httpService: HttpService }) { + this.httpService = params.httpService; + } + + public async getUserKey(): Promise { + const response = await this.httpService.get("/register/") + return response.text(); + } +} + +export class UserKeyStoreLocalStorage { + public getUserKey(): Promise { + const userKey = storage.getString(user_key) + return Promise.resolve(userKey === null ? undefined : userKey) + } + + public storeUserKey(userKey: string): void { + storage.set(user_key, userKey) + } +} diff --git a/mobile/src/theme/DarkTheme.ts b/mobile/src/theme/DarkTheme.ts new file mode 100644 index 0000000..625aff2 --- /dev/null +++ b/mobile/src/theme/DarkTheme.ts @@ -0,0 +1,33 @@ +import { Theme } from "./Theme"; + +// https://colorhunt.co/palette/525252414141313131ec625f +const colors = { + primary: "#414141", + secondary: "#313131", + tertiary: "#606060", + quaternary: "#808080", +}; + +const colorSpecial = "#337a44"; + +const darkTheme: Theme = { + id: "2", + name: "Dark Theme", + themePreviewColor: colors.primary, + background: colors.primary, + appBarBgColor: colors.secondary, + textColor: colors.primary, + textLightColor: "#AAAAAA", + playerTurnColor: colors.tertiary, + boardColor: colors.secondary, + pitColor: colors.tertiary, + pitSelectedColor: colors.secondary, + stoneColor: "#252525", + stoneLightColor: "#252525", + pitGameMoveAnimateColor: colors.quaternary, + pitEmptyPitAnimateColor: colorSpecial, + pitLastStoneInBankPitAnimateColor: colorSpecial, + pitGetRivalStonePitAnimateColor: colorSpecial, +}; + +export default darkTheme; diff --git a/mobile/src/theme/GreyTheme.ts b/mobile/src/theme/GreyTheme.ts new file mode 100644 index 0000000..448e8c9 --- /dev/null +++ b/mobile/src/theme/GreyTheme.ts @@ -0,0 +1,23 @@ +import { Theme } from "./Theme"; + +const greyTheme: Theme = { + id: "1", + name: "Grey Theme", + themePreviewColor: "#4D606E", + background: "#EEEEEE", + appBarBgColor: "#e4e4e4", + textColor: "#4D606E", + textLightColor: "#EEEEEE", + playerTurnColor: "#84b8a6", + boardColor: "#4D606E", + pitColor: "#D3D4D8", + pitSelectedColor: "#8837fa", + stoneColor: "#393E46", + stoneLightColor: "#EEEEEE", + pitGameMoveAnimateColor: "#c9b43c", + pitEmptyPitAnimateColor: "#5d7322", + pitLastStoneInBankPitAnimateColor: "#9463f7", + pitGetRivalStonePitAnimateColor: "#ff3d44", +}; + +export default greyTheme; diff --git a/mobile/src/theme/LightTheme.ts b/mobile/src/theme/LightTheme.ts new file mode 100644 index 0000000..e81d5e4 --- /dev/null +++ b/mobile/src/theme/LightTheme.ts @@ -0,0 +1,25 @@ +import { Theme } from "./Theme"; + +const colorSpecial = "#8B8B8B"; + +const lightTheme: Theme = { + id: "1", + name: "Light Theme", + themePreviewColor: "#9B9B9B", + background: "#BBBBBB", + appBarBgColor: "#7B7B7B", + textColor: "#5B5B5B", + textLightColor: "#EBEBEB", + playerTurnColor: "#6B6B6B", + boardColor: "#9B9B9B", + pitColor: "#B8B8B8", + pitSelectedColor: "#9B9B9B", + stoneColor: "#5B5B5B", + stoneLightColor: "#3B3B3B", + pitGameMoveAnimateColor: "#ABABAB", + pitEmptyPitAnimateColor: colorSpecial, + pitLastStoneInBankPitAnimateColor: colorSpecial, + pitGetRivalStonePitAnimateColor: colorSpecial, +}; + +export default lightTheme; diff --git a/mobile/src/theme/Theme.ts b/mobile/src/theme/Theme.ts new file mode 100644 index 0000000..11fa3f6 --- /dev/null +++ b/mobile/src/theme/Theme.ts @@ -0,0 +1,19 @@ +export type Theme = { + id: string; + name: string; + themePreviewColor: string; // for theme switch menu + textColor: string; + textLightColor: string; + background: string; + appBarBgColor: string; + playerTurnColor: string; + boardColor: string; + pitColor: string; + pitSelectedColor: string; + stoneColor: string; + stoneLightColor: string; + pitGameMoveAnimateColor: string; + pitEmptyPitAnimateColor: string; + pitLastStoneInBankPitAnimateColor: string; + pitGetRivalStonePitAnimateColor: string; +}; diff --git a/mobile/src/theme/ThemeManager.ts b/mobile/src/theme/ThemeManager.ts new file mode 100644 index 0000000..8195b7f --- /dev/null +++ b/mobile/src/theme/ThemeManager.ts @@ -0,0 +1,51 @@ +import lightTheme from "./LightTheme"; +import greyTheme from "./GreyTheme"; +import { Theme } from "./Theme"; +import darkTheme from "./DarkTheme"; +import { TinyEmitter } from "tiny-emitter"; +import { storage } from "../storage"; + +export const themes = [lightTheme, darkTheme, greyTheme]; + +const THEME_ID = "theme_id"; + +export type ThemeManagerEvents = "themechange"; + +export default class ThemeManager extends TinyEmitter { + private _theme: Theme; + constructor() { + super(); + this._theme = this.readFromLocalStorage() || lightTheme; + } + + public get theme() { + return this._theme; + } + + public set theme(value: Theme) { + this._theme = value; + this.emit("themechange", value); + this.writetToLocalStorage(value); + } + + private writetToLocalStorage(value: Theme) { + storage.set(THEME_ID, value.id); + } + + private readFromLocalStorage(): Theme | undefined { + const themeID = storage.getString(THEME_ID); + const theme = themes.find((eachTheme: Theme) => themeID === eachTheme.id); + return theme; + } + + public get themes(): Theme[] { + return themes; + } + + public on(event: ThemeManagerEvents, callback: (...value: any[]) => void): this { + return super.on(event, callback); + } + public off(event: ThemeManagerEvents, callback: (...value: any[]) => void): this { + return super.off(event, callback); + } +} diff --git a/mobile/src/types/index.ts b/mobile/src/types/index.ts index 68c4b85..5d7cc02 100644 --- a/mobile/src/types/index.ts +++ b/mobile/src/types/index.ts @@ -1,9 +1,10 @@ import type { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { Context } from '../context/context'; export type RootStackParamList = { - Home: undefined, - Loby: undefined, - Game: { gameId: string } + Home: { context: Context }, + Loby: { context: Context }, + Game: { context: Context, gameId: string } }; export type HomeScreenProps = NativeStackScreenProps; diff --git a/mobile/yarn.lock b/mobile/yarn.lock index 328665e..8f7f93c 100644 --- a/mobile/yarn.lock +++ b/mobile/yarn.lock @@ -4861,6 +4861,11 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" +mancala.js@^0.0.2-beta.3: + version "0.0.2-beta.3" + resolved "https://registry.yarnpkg.com/mancala.js/-/mancala.js-0.0.2-beta.3.tgz#78edfa220e1a7172351a07f255eb81180845226a" + integrity sha512-LPmQ/VT4/JWFdp/YSB7k63zK7GyflApyh4M26t23a9uXFRSpBcWSePtNFpHU/xY2+1gVjlbOwQjup2QW3Tue7w== + marky@^1.2.2: version "1.2.5" resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" @@ -5639,6 +5644,11 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-native-mmkv@^2.12.2: + version "2.12.2" + resolved "https://registry.yarnpkg.com/react-native-mmkv/-/react-native-mmkv-2.12.2.tgz#4bba0f5f04e2cf222494cce3a9794ba6a4894dee" + integrity sha512-6058Aq0p57chPrUutLGe9fYoiDVDNMU2PKV+lLFUJ3GhoHvUrLdsS1PDSCLr00yqzL4WJQ7TTzH+V8cpyrNcfg== + react-native-safe-area-context@^4.9.0: version "4.9.0" resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-4.9.0.tgz#21a570ca3594cb4259ba65f93befaa60d91bcbd0" @@ -5652,6 +5662,11 @@ react-native-screens@^3.29.0: react-freeze "^1.0.0" warn-once "^0.1.0" +react-native-snackbar@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/react-native-snackbar/-/react-native-snackbar-2.6.2.tgz#7c0d0d93bfb5822fb1f41f00d29383f522c02185" + integrity sha512-edAubZJiBowwQUXJV5oXbMqitQ9vw7JzWUCczeTPVo6lRa+FzsUjiCQBHdWBbCw/N8/Q7jgKg4juNXU/bXZdXg== + react-native@0.73.6: version "0.73.6" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.73.6.tgz#ed4c675e205a34bd62c4ce8b9bd1ca5c85126d5b"