[mobile] feature: add context, http service, store, storage

This commit is contained in:
Halit Aksoy 2024-03-25 10:04:03 +03:00
parent c330c9d4f5
commit 341fe0e083
25 changed files with 544 additions and 16 deletions

View File

@ -13,11 +13,14 @@
"@react-navigation/native": "^6.1.17", "@react-navigation/native": "^6.1.17",
"@react-navigation/native-stack": "^6.9.26", "@react-navigation/native-stack": "^6.9.26",
"i18next": "^23.10.1", "i18next": "^23.10.1",
"mancala.js": "^0.0.2-beta.3",
"react": "18.2.0", "react": "18.2.0",
"react-i18next": "^14.1.0", "react-i18next": "^14.1.0",
"react-native": "0.73.6", "react-native": "0.73.6",
"react-native-mmkv": "^2.12.2",
"react-native-safe-area-context": "^4.9.0", "react-native-safe-area-context": "^4.9.0",
"react-native-screens": "^3.29.0", "react-native-screens": "^3.29.0",
"react-native-snackbar": "^2.6.2",
"tiny-emitter": "^2.1.0" "tiny-emitter": "^2.1.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,19 +1,80 @@
import * as React from 'react'; 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 { createNativeStackNavigator } from '@react-navigation/native-stack';
import { RootStackParamList } from './types'; import { RootStackParamList } from './types';
import { HomeScreen } from './screens/HomeScreen'; import { HomeScreen } from './screens/HomeScreen';
import LobyScreen from './screens/LobyScreen'; 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<RootStackParamList>(); const Stack = createNativeStackNavigator<RootStackParamList>();
const context = initContext();
function App() { function App() {
const { t } = useTranslation();
const [userKey, setUserKey] = React.useState<string | undefined>(undefined);
const [connectionState, setConnetionState] = React.useState<ConnectionState>("connecting");
//@ts-ignore
const [theme, setTheme] = React.useState<Theme>(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 ( return (
<NavigationContainer> <NavigationContainer>
<Stack.Navigator> <Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="Home" component={HomeScreen} initialParams={{context}} />
<Stack.Screen name="Loby" component={LobyScreen} /> <Stack.Screen name="Loby" component={LobyScreen} />
<Stack.Screen name="Game" component={GameScreen} />
</Stack.Navigator> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );

View File

@ -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
};
};

View File

@ -1,6 +1,39 @@
export default { export default {
translation: { translation: {
newGame: "New Game", Mancala: "Mancala",
searchingOppenent: 'Please wait, searching opponent...', 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",
} }
}; };

View File

@ -1,6 +1,39 @@
export default { export default {
translation: { translation: {
newGame: "Yeni Oyun", Mancala: "Köçürme",
searchingOppenent: 'Lütfen bekleyin, rakip bulunuyor...', 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"
} }
}; };

13
mobile/src/models/Game.ts Normal file
View File

@ -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;
}

View File

@ -0,0 +1,3 @@
export interface GameMove {
index: number;
}

View File

@ -0,0 +1,46 @@
export type LoadingStateType = "unset" | "loading" | "loaded" | "error";
export class LoadingState<T> {
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<T>() {
return new LoadingState<T>({ state: "unset" });
}
public static Loading<T>() {
return new LoadingState<T>({ state: "loading" });
}
public static Error<T>(props: { errorMessage: string }) {
const { errorMessage } = props;
return new LoadingState<T>({ state: "error", errorMessage });
}
public static Loaded<T>(props: { value?: T }) {
const { value } = props;
return new LoadingState<T>({ 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";
}
}

View File

@ -0,0 +1,6 @@
export interface User {
id: string;
name: string;
isOnline: boolean;
isAnonymous: boolean;
}

View File

@ -0,0 +1,4 @@
export interface UserConnectionInfo {
userId: string;
isOnline: boolean;
}

View File

@ -109,7 +109,7 @@ export class RTMTWS extends TinyEmitter implements RTMT {
this.emit(MESSAGE_CHANNEL_PREFIX + channel, message); 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); return super.on(event, callback);
} }

View File

@ -1,13 +1,16 @@
import * as React from 'react'; import * as React from 'react';
import { View, Button } from 'react-native'; import { View, Button, Text } from 'react-native';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { GameScreenProps } from '../types'; import { GameScreenProps } from '../types';
export function GameScreen({ navigation, route }: GameScreenProps) { export function GameScreen({ navigation, route }: GameScreenProps) {
const { context, gameId } = route.params;
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>{gameId}</Text>
</View> </View>
); );
} }

View File

@ -4,13 +4,15 @@ import { useTranslation } from 'react-i18next';
import { HomeScreenProps } from '../types'; import { HomeScreenProps } from '../types';
export function HomeScreen({ navigation, route }: HomeScreenProps) { export function HomeScreen({ navigation, route }: HomeScreenProps) {
const { context } = route.params;
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button <Button
title={t('newGame')} title={t('NewGame')}
onPress={() => navigation.navigate('Loby')}></Button> onPress={() => navigation.navigate('Loby', { context })}></Button>
</View> </View>
); );
} }

View File

@ -2,13 +2,31 @@ import * as React from 'react';
import { View, Text, Button } from 'react-native'; import { View, Text, Button } from 'react-native';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { LobyScreenProps } from '../types'; 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) { export default function LobyScreen({ navigation, route }: LobyScreenProps) {
const { context } = route.params;
const { t } = useTranslation(); 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 ( return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>{t('searchingOppenent')}</Text> <Text>{t('SearchingOpponent')}</Text>
</View> </View>
); );
} }

View File

@ -0,0 +1,23 @@
import { server } from "../const/config";
export interface HttpService {
get: (route: string) => Promise<Response>;
}
export class HttpServiceImpl implements HttpService {
public serverAdress: string;
constructor(serverAdress: string) {
this.serverAdress = serverAdress;
}
public async get(route: string): Promise<Response> {
const url = server.serverAdress + route;
const requestOptions = {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
};
const response = await fetch(url, requestOptions);
return response;
}
}

View File

@ -0,0 +1,3 @@
import { MMKV } from 'react-native-mmkv'
export const storage = new MMKV()

View File

@ -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<Game | undefined>;
}
export class GameStoreImpl implements GameStore {
httpService: HttpService;
constructor(props: { httpService: HttpService }) {
this.httpService = props.httpService;
}
async get(id: string): Promise<Game | undefined> {
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);
}
}
}

View File

@ -0,0 +1,54 @@
import { HttpService } from "../service/HttpService"
import { storage } from "../storage";
const user_key = "user_key"
export interface UserKeyStore {
getUserKey: () => Promise<string>;
}
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<string> {
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<string> {
const response = await this.httpService.get("/register/")
return response.text();
}
}
export class UserKeyStoreLocalStorage {
public getUserKey(): Promise<string | undefined> {
const userKey = storage.getString(user_key)
return Promise.resolve(userKey === null ? undefined : userKey)
}
public storeUserKey(userKey: string): void {
storage.set(user_key, userKey)
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

19
mobile/src/theme/Theme.ts Normal file
View File

@ -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;
};

View File

@ -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);
}
}

View File

@ -1,9 +1,10 @@
import type { NativeStackScreenProps } from '@react-navigation/native-stack'; import type { NativeStackScreenProps } from '@react-navigation/native-stack';
import { Context } from '../context/context';
export type RootStackParamList = { export type RootStackParamList = {
Home: undefined, Home: { context: Context },
Loby: undefined, Loby: { context: Context },
Game: { gameId: string } Game: { context: Context, gameId: string }
}; };
export type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>; export type HomeScreenProps = NativeStackScreenProps<RootStackParamList, 'Home'>;

View File

@ -4861,6 +4861,11 @@ makeerror@1.0.12:
dependencies: dependencies:
tmpl "1.0.5" 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: marky@^1.2.2:
version "1.2.5" version "1.2.5"
resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" 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" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== 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: react-native-safe-area-context@^4.9.0:
version "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" 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" react-freeze "^1.0.0"
warn-once "^0.1.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: react-native@0.73.6:
version "0.73.6" version "0.73.6"
resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.73.6.tgz#ed4c675e205a34bd62c4ce8b9bd1ca5c85126d5b" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.73.6.tgz#ed4c675e205a34bd62c4ce8b9bd1ca5c85126d5b"