diff --git a/README.md b/README.md index c53f4fa..ca69fd3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MagicBell Mobile Inbox -This repo contains an open source mobile client for the MagicBell API, build in React Native. You can use it as an example project on how to setup a React Native app that integrates with MagicBell notifications and push notifications via APNs and FCM. +This repo contains an open source mobile client for the MagicBell API, built in React Native. You can use it as an example project on how to setup a React Native app that integrates with MagicBell notifications and push notifications via APNs and FCM. To explore the full feature set of MagicBell, and to dive deeper into the API please refer to the [documentation](https://www.magicbell.com/docs). @@ -31,7 +31,9 @@ In order to build the app you will need to have the native tool chains for the p You will also need [NodeJS](https://nodejs.org) and [Yarn](https://yarnpkg.com) for obvious reasons. -More details on how to set up a React Native dev environment can be found on [reactnative.dev](https://reactnative.dev/docs/environment-setup). +More details on how to set up a React Native dev environment can be found on [reactnative.dev](https://reactnative.dev/docs/set-up-your-environment). + +Install the dependencies by running `yarn`. ## Starting A Local Development Build @@ -101,6 +103,26 @@ At this point you can use the keyboard shortcuts on the dashboard to build and o If you want to launch the app on a specific simulator or even device, you can use the `shift+i`/`shift+a` shortcuts, or start another build process from the terminal while keeping Metro in the background by running `yarn ios` or `yarn android` (both of which support additional parameters that can be inspected by passing `-h`). +## Sending FCM Notifications +To send notifications with FCM, you will need a [Firebase account](https://firebase.google.com/) and an Android app. You can create the app by using the `Add app` button on your console and selecting android. +You should now see a button that says, `google-service.json`, using which you can download the `google-service.json` file. + +If you have a pre-registered app then you can go into the Project Settings of the app, and in the General tab, you can find the button to download `google-service.json` in the Your apps section. + +After downloading the file replace `google-service.json` file in the root of this project with your file. + +To launch the Android app you can use: +```bash +yarn android:clean +``` + +The command will do a clean Android build and launch the Android app in an emulator. + +For authentication, you will need a MagicBell userJWT, you can [generate it using your MagicBell API Key and the external ID of the user](https://www.magicbell.com/docs/api/authentication/user) you want to send notifications to. + +To test if you are receiving notifications correctly, you can use the [FCM Test](https://www.magicbell.com/test/fcm). + +You will need an Admin SDK private key, you can get it from your firebase console by going to the Project Settings by clicking on the gear button on the left sidebar. Then going to Service Accounts and clicking the `Generate new private key`, it will save a JSON file to your machine that you can then upload to the [MagicBell FCM Test](https://www.magicbell.com/test/fcm) page. ## Building Release Builds diff --git a/google-services.json b/google-services.json index ec45d35..f447205 100644 --- a/google-services.json +++ b/google-services.json @@ -1,13 +1,13 @@ { "project_info": { - "project_number": "371921703332", - "project_id": "react-native-starter-ee960", - "storage_bucket": "react-native-starter-ee960.firebasestorage.app" + "project_number": "922199420286", + "project_id": "mb-fcm-niya", + "storage_bucket": "mb-fcm-niya.firebasestorage.app" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:371921703332:android:f0cc04373ce55917617131", + "mobilesdk_app_id": "1:922199420286:android:31042b682826f9fd4e172f", "android_client_info": { "package_name": "com.magicbell.mobileinbox" } @@ -15,26 +15,7 @@ "oauth_client": [], "api_key": [ { - "current_key": "AIzaSyD4WB5GlZApHn66O1CM2r_z7z9Qiis_g_Y" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [] - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:371921703332:android:c46fd837b7edbe43617131", - "android_client_info": { - "package_name": "com.rnprototype" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "AIzaSyD4WB5GlZApHn66O1CM2r_z7z9Qiis_g_Y" + "current_key": "AIzaSyB3rwPM8HRM3iqzWar_vfsP5_oqQlK6-pc" } ], "services": { diff --git a/index.js b/index.js index ffe0f07..4b31f9e 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,7 @@ import { registerRootComponent } from 'expo'; import App from './src/App'; -// Polyfills needed for @magicbell/react-headless -import EventSource from 'react-native-sse'; +// Polyfills for React Native environment import 'react-native-url-polyfill/auto'; -global.EventSource = EventSource; registerRootComponent(App); diff --git a/ios/ci_scripts/ci_post_clone.sh b/ios/ci_scripts/ci_post_clone.sh deleted file mode 100755 index 376f080..0000000 --- a/ios/ci_scripts/ci_post_clone.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -set -e -echo "Running ci_post_clone.sh" - -# cd out of ios/ci_scripts into main project directory -cd ../../ - -# install node and cocoapods -HOMEBREW_NO_AUTO_UPDATE=1 brew install node cocoapods -npm install - -npx expo prebuild --platform=ios \ No newline at end of file diff --git a/package.json b/package.json index 731e2d4..b963c55 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,13 @@ "expo": "^52.0.27", "expo-app-loading": "^2.1.1", "expo-application": "~6.0.2", + "expo-dev-client": "~5.0.20", "expo-font": "~13.0.3", "expo-linking": "~7.0.4", "expo-notifications": "~0.29.12", "expo-splash-screen": "~0.29.21", "lodash.isequal": "^4.5.0", + "magicbell-js": "^1.4.0", "native-base": "^3.4.28", "react": "18.3.1", "react-dom": "^18.3.1", diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 501f5b7..fb9dfed 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import { ButtonProps, StyleSheet, Text, TouchableOpacity } from 'react-native'; import { colors } from '../constants'; import Svg, { Circle } from 'react-native-svg'; diff --git a/src/components/MagicBellProvider.tsx b/src/components/MagicBellProvider.tsx index 0123695..dac8cde 100644 --- a/src/components/MagicBellProvider.tsx +++ b/src/components/MagicBellProvider.tsx @@ -1,23 +1,171 @@ -import * as MagicBell from '@magicbell/react-headless'; -import React, { PropsWithChildren } from 'react'; +import React, { createContext, useContext, useState, useCallback, useMemo, ReactNode } from 'react'; +import { Client, Notification } from 'magicbell-js/user-client'; import { useCredentials } from '../hooks/useAuth'; -interface IProps {} -export default function MagicBellProvider({ children }: PropsWithChildren) { - const [credentials] = useCredentials(); +type ListNotificationsParams = { + limit?: number; + startingAfter?: string; + endingBefore?: string; + status?: string; + category?: string; + topic?: string; +}; + +type MagicBellContextType = { + client: Client | null; + notifications: Notification[]; + isLoading: boolean; + error: Error | null; + fetchNotifications: (params?: ListNotificationsParams) => Promise; + refreshNotifications: () => Promise; + markAsRead: (notificationId: string) => Promise; + markAsUnread: (notificationId: string) => Promise; + archiveNotification: (notificationId: string) => Promise; +}; + +const MagicBellContext = createContext(undefined); - if (credentials) { - return ( - - <>{children} - - ); +export const useMagicBell = () => { + const context = useContext(MagicBellContext); + if (!context) { + throw new Error('useMagicBell must be used within MagicBellProvider'); } + return context; +}; + +type MagicBellProviderProps = { + children: ReactNode; +}; + +export default function MagicBellProvider({ children }: MagicBellProviderProps) { + const [credentials] = useCredentials(); + const [notifications, setNotifications] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const client = useMemo(() => { + if (!credentials?.userJWT) { + return null; + } + return new Client({ + token: credentials.userJWT, + baseUrl: credentials.serverURL, + }); + }, [credentials?.userJWT, credentials?.serverURL]); + + const fetchNotifications = useCallback( + async (params?: ListNotificationsParams) => { + if (!client) { + setError(new Error('MagicBell client not initialized')); + return; + } + + setIsLoading(true); + setError(null); + + try { + const response = await client.notifications.listNotifications({ + limit: params?.limit || 50, + ...params, + }); + + setNotifications(response.data?.data || []); + } catch (err) { + const error = err instanceof Error ? err : new Error('Failed to fetch notifications'); + setError(error); + console.error('Error fetching notifications:', error); + setNotifications([]); + } finally { + setIsLoading(false); + } + }, + [client], + ); + + const refreshNotifications = useCallback(async () => { + await fetchNotifications(); + }, [fetchNotifications]); + + const markAsRead = useCallback( + async (notificationId: string) => { + if (!client) return; + + try { + await client.notifications.markNotificationRead(notificationId); + + setNotifications((prev) => + prev.map((notification) => + notification.id === notificationId ? { ...notification, readAt: new Date().toISOString() } : notification, + ), + ); + } catch (err) { + console.error('Error marking notification as read:', err); + throw err; + } + }, + [client], + ); + + const markAsUnread = useCallback( + async (notificationId: string) => { + if (!client) return; + + try { + await client.notifications.markNotificationUnread(notificationId); + + setNotifications((prev) => + prev.map((notification) => + notification.id === notificationId ? { ...notification, readAt: null } : notification, + ), + ); + } catch (err) { + console.error('Error marking notification as unread:', err); + throw err; + } + }, + [client], + ); + + const archiveNotification = useCallback( + async (notificationId: string) => { + if (!client) return; + + try { + await client.notifications.archiveNotification(notificationId); + + setNotifications((prev) => prev.filter((notification) => notification.id !== notificationId)); + } catch (err) { + console.error('Error archiving notification:', err); + throw err; + } + }, + [client], + ); + + const value = useMemo( + () => ({ + client, + notifications, + isLoading, + error, + fetchNotifications, + refreshNotifications, + markAsRead, + markAsUnread, + archiveNotification, + }), + [ + client, + notifications, + isLoading, + error, + fetchNotifications, + refreshNotifications, + markAsRead, + markAsUnread, + archiveNotification, + ], + ); - return children; + return {children}; } diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx index c578c82..992fb84 100644 --- a/src/components/Notification.tsx +++ b/src/components/Notification.tsx @@ -1,4 +1,4 @@ -import { IRemoteNotification } from '@magicbell/react-headless'; +import { Notification as NotificationType } from 'magicbell-js/user-client'; import React from 'react'; import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; import { navigationRef } from '../Navigator'; @@ -6,7 +6,7 @@ import { CommonActions } from '@react-navigation/native'; import { colors, routes } from '../constants'; interface IProps { - data: IRemoteNotification; + data: NotificationType; } const styles = StyleSheet.create({ @@ -76,8 +76,8 @@ export default function Notification(props: IProps) { } }; - // convert sentAt timestamp to a human-readable format such as "2 hours ago" - const sentAt = new Date(+props.data.sentAt! * 1000); + // convert createdAt timestamp to a human-readable format such as "2 hours ago" + const sentAt = new Date(props.data.createdAt); const sentAtString = convertTimestamp(sentAt); return ( diff --git a/src/components/TextInput.tsx b/src/components/TextInput.tsx index 99e72df..eaf2780 100644 --- a/src/components/TextInput.tsx +++ b/src/components/TextInput.tsx @@ -17,5 +17,5 @@ const styles = StyleSheet.create({ }); export default function CustomTextInput(props: TextInputProps) { - return ; + return ; } diff --git a/src/constants.ts b/src/constants.ts index 910c2f1..4efad5c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -75,22 +75,17 @@ export const routes = { export const config: { [key: string]: Credentials } = { prod: { - apiKey: 'd6a3cf19179a45a5daa9ac7f3f37e9d49914d2ad', - userEmail: 'matt@magicbell.io', - userHmac: '5n4ooUtzydnYq5GYh6PIWGeP2alepTf/Qgb/Sp/g3Co=', - serverURL: 'https://api.magicbell.com', + serverURL: 'https://api.magicbell.com/v2', + userJWT: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2VtYWlsIjpudWxsLCJ1c2VyX2V4dGVybmFsX2lkIjoiN2Y0YmFhYjUtMGM5MS00NGU4LThiNTgtNWZmODQ5NTM1MTc0IiwiYXBpX2tleSI6IjVmNWNmYjI5NTEzODQ2NDMzZTgxYjkxZWM1ZTkwOGM5NDNmZjYwNTgiLCJpYXQiOjE3NjM1NDMyOTksImV4cCI6MTc2MzYyOTY5OX0.NzZcuIv_g-nW0JAhF0i_pH4T96BHCfkdjkJOLnqvF6M', }, local: { - apiKey: '8cd17191a14339cb1d4e58c4ea471eeca51d2c70', - userEmail: 'matt@magicbell.io', - userHmac: '', serverURL: 'https://1b35-79-153-3-135.ngrok-free.app', + userJWT: '', }, review: { - apiKey: '552efd58f59315d065e45b07f8d8f8a2751c2b5b', - userEmail: 'matthewoxley001@gmail.com', - userHmac: '5n4ooUtzydnYq5GYh6PIWGeP2alepTf/Qgb/Sp/g3Co=', serverURL: 'https://api-4374.magicbell.cloud/', + userJWT: '', }, }; diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 927f528..27b0984 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -1,16 +1,14 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; -import { UserClient } from 'magicbell/user-client'; +import { Client } from 'magicbell-js/user-client'; import useDeviceToken from './useDeviceToken'; const storageKey = 'mb'; export type Credentials = { - apiKey: string; - userEmail: string; - userHmac: string; serverURL: string; + userJWT: string; }; type CredentialsContextType = { @@ -64,19 +62,17 @@ const getCredentials = async () => { return null; } try { - const { apiKey, userEmail, userHmac, serverURL } = JSON.parse(value); - const client = new UserClient({ - apiKey: apiKey, - userEmail: userEmail, - userHmac: userHmac, - host: serverURL, - }); - const config = await client.request({ - method: 'GET', - path: '/config', + const { serverURL, userJWT } = JSON.parse(value); + + const client = new Client({ + token: userJWT, + baseUrl: serverURL, }); - if (config) { - return { apiKey, userEmail, userHmac, serverURL }; + + // TODO: Verify bad credentials cannot be used + // Use the client to check the credentials are valid + if (client.config) { + return { serverURL, userJWT }; } } catch (e) { console.error('Error parsing credentials', e); diff --git a/src/hooks/useDeviceToken.tsx b/src/hooks/useDeviceToken.tsx index 50f6d9a..16583ab 100644 --- a/src/hooks/useDeviceToken.tsx +++ b/src/hooks/useDeviceToken.tsx @@ -5,42 +5,35 @@ import { getIosPushNotificationServiceEnvironmentAsync, } from 'expo-application'; import { getDevicePushTokenAsync, requestPermissionsAsync } from 'expo-notifications'; -import { UserClient } from 'magicbell/user-client'; +import { ApnsTokenPayload, ApnsTokenPayloadInstallationId, Client, FcmTokenPayload } from 'magicbell-js/user-client'; import React, { useEffect } from 'react'; import { Platform } from 'react-native'; import { Credentials } from './useAuth'; const clientWithCredentials = (credentials: Credentials) => - new UserClient({ - apiKey: credentials.apiKey, - userEmail: credentials.userEmail, - userHmac: credentials.userHmac, - host: credentials.serverURL, + new Client({ + token: credentials.userJWT, + baseUrl: credentials.serverURL, }); -const tokenPath = Platform.select({ - ios: '/channels/mobile_push/apns/tokens', - android: '/channels/mobile_push/fcm/tokens', -})!; - -const apnsTokenPayload = async (token: string): Promise => { +const apnsTokenPayload = async (token: string): Promise => { const isSimulator = (await getIosApplicationReleaseTypeAsync()) === ApplicationReleaseType.SIMULATOR; const installationId = - (await getIosPushNotificationServiceEnvironmentAsync()) || isSimulator ? 'development' : 'production'; + (await getIosPushNotificationServiceEnvironmentAsync()) || isSimulator + ? ApnsTokenPayloadInstallationId.DEVELOPMENT + : ApnsTokenPayloadInstallationId.PRODUCTION; + + const appId = applicationId ?? undefined; return { - apns: { - device_token: token, - installation_id: installationId, - app_id: applicationId, - }, + deviceToken: token, + installationId, + appId, }; }; -const fcmTokenPayload = (token: string): any => { +const fcmTokenPayload = (token: string): FcmTokenPayload => { return { - fcm: { - device_token: token, - }, + deviceToken: token, }; }; @@ -49,28 +42,25 @@ const registerTokenWithCredentials = async (token: string, credentials: Credenti console.log('posting token', token); const client = clientWithCredentials(credentials); - client - .request({ - method: 'POST', - path: tokenPath, - data: data, - }) - .catch((err) => { - console.log('post token error', err); - }); + + switch (Platform.OS) { + case 'ios': + client.channels.saveApnsToken(data); + case 'android': + client.channels.saveFcmToken(data); + } }; const unregisterTokenWithCredentials = async (token: string, credentials: Credentials) => { console.log('deleting token', token); const client = clientWithCredentials(credentials); - client - .request({ - method: 'DELETE', - path: tokenPath + '/' + token, - }) - .catch((err) => { - console.log('delete token error', err); - }); + + switch (Platform.OS) { + case 'ios': + client.channels.deleteApnsToken(token); + case 'android': + client.channels.deleteFcmToken(token); + } }; export default function useDeviceToken(credentials: Credentials | null | undefined) { diff --git a/src/hooks/usePushNotificationHandler.tsx b/src/hooks/usePushNotificationHandler.tsx index 58a4b07..20fae06 100644 --- a/src/hooks/usePushNotificationHandler.tsx +++ b/src/hooks/usePushNotificationHandler.tsx @@ -1,5 +1,3 @@ -import PushNotificationIOS, { PushNotification } from '@react-native-community/push-notification-ios'; - import { navigationRef } from '../Navigator'; import { useEffect } from 'react'; import { CommonActions } from '@react-navigation/native'; diff --git a/src/hooks/useReviewCredentials.tsx b/src/hooks/useReviewCredentials.tsx index e65d448..d14e009 100644 --- a/src/hooks/useReviewCredentials.tsx +++ b/src/hooks/useReviewCredentials.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { Credentials } from './useAuth'; import { useURLMemo } from './useURLMemo'; @@ -10,19 +10,15 @@ import { Platform } from 'react-native'; * ATTENTION: This is only for MagicBell internal use. You should not follow this example in your production app. * * Example URL: - * x-magicbell-review://connect?apiHost=[...]&apiKey=[...]&userEmail=[...]&userHmac=[...] + * x-magicbell-review://connect?apiHost=[...]&userJWT=[...] * */ const parseLaunchURLCredentials = (url: URL): Credentials | null => { var serverURL = url.searchParams.get('apiHost'); - const apiKey = url.searchParams.get('apiKey'); - // TODO: support userExternalID as well - const userEmail = url.searchParams.get('userEmail'); + const userJWT = url.searchParams.get('userJWT'); - const userHmac = url.searchParams.get('userHmac'); - - if (!serverURL || !apiKey || !userEmail || !userHmac) { + if (!serverURL || !userJWT) { console.warn('Could not parse credentials from launch URL: ', url.toString()); return null; } @@ -37,9 +33,7 @@ const parseLaunchURLCredentials = (url: URL): Credentials | null => { const credentials: Credentials = { serverURL, - apiKey, - userHmac, - userEmail, + userJWT, }; return credentials; }; diff --git a/src/screens/Details.tsx b/src/screens/Details.tsx index 33917be..1b2f555 100644 --- a/src/screens/Details.tsx +++ b/src/screens/Details.tsx @@ -3,7 +3,7 @@ import { StyleSheet, Text, View } from 'react-native'; import { colors, styles } from '../constants'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { convertTimestamp } from '../components/Notification'; -import { IRemoteNotification } from '@magicbell/react-headless'; +import { Notification } from 'magicbell-js/user-client'; const s = StyleSheet.create({ sectionContainer: { @@ -43,8 +43,8 @@ const s = StyleSheet.create({ export default function Details(props: NativeStackScreenProps) { console.log('props', props.route.params); - const params = props.route.params as IRemoteNotification; - const sentAtString = convertTimestamp(new Date(params!.sentAt * 1000)); + const params = props.route.params as Notification; + const sentAtString = convertTimestamp(new Date(params!.createdAt)); return ( diff --git a/src/screens/Home.tsx b/src/screens/Home.tsx index bf072d3..d42cc0a 100644 --- a/src/screens/Home.tsx +++ b/src/screens/Home.tsx @@ -1,22 +1,28 @@ -import React from 'react'; +import React, { useEffect } from 'react'; -import { Button, SafeAreaView, ScrollView } from 'react-native'; +import { Button, SafeAreaView, ScrollView, ActivityIndicator, Text } from 'react-native'; import { styles } from '../constants'; import { useCredentials } from '../hooks/useAuth'; -import { useNotifications } from '@magicbell/react-headless'; +import { useMagicBell } from '../components/MagicBellProvider'; import Notification from '../components/Notification'; import usePushNotificationHandler from '../hooks/usePushNotificationHandler'; export default function HomeScreen(): React.JSX.Element { const [_, __, logout] = useCredentials(); - const store = useNotifications(); + const { notifications, isLoading, error, fetchNotifications } = useMagicBell(); usePushNotificationHandler(); + useEffect(() => { + fetchNotifications(); + }, [fetchNotifications]); + return ( - {store?.notifications.map((notification) => ( + {isLoading && } + {error && Error: {error.message}} + {notifications?.map((notification) => ( ))} diff --git a/src/screens/SignIn.tsx b/src/screens/SignIn.tsx index 7c6f2e0..d6adf50 100644 --- a/src/screens/SignIn.tsx +++ b/src/screens/SignIn.tsx @@ -16,24 +16,20 @@ export const SignInScreen = (): React.JSX.Element => { const defaultCredentials = reviewCredentials || currentConfig; const [loading, setLoading] = useState(false); const [serverURL, setServerURL] = useState(defaultCredentials.serverURL); - const [apiKey, setApiKey] = useState(defaultCredentials.apiKey); - const [userEmail, setUserEmail] = useState(defaultCredentials.userEmail); - const [userHmac, setUserHmac] = useState(defaultCredentials.userHmac); + const [userJWT, setUserJWT] = useState(defaultCredentials.userJWT); useEffect(() => { if (reviewCredentials) { setServerURL(reviewCredentials.serverURL); - setApiKey(reviewCredentials.apiKey); - setUserEmail(reviewCredentials.userEmail); - setUserHmac(reviewCredentials.userHmac); + setUserJWT(reviewCredentials.userJWT); } }, [reviewCredentials]); const handleSubmit = useCallback(async () => { setLoading(true); - await signIn({ apiKey, userEmail, userHmac, serverURL }); + await signIn({ serverURL, userJWT }); setLoading(false); - }, [signIn, apiKey, userEmail, userHmac, serverURL]); + }, [signIn, userJWT, serverURL]); if (credentials) { throw new Error('User is already signed in'); @@ -75,10 +71,8 @@ export const SignInScreen = (): React.JSX.Element => { onValueChange={ ((itemValue: keyof typeof config) => { const c = config[itemValue]; - setApiKey(c.apiKey); - setUserEmail(c.userEmail); - setUserHmac(c.userHmac); setServerURL(c.serverURL); + setUserJWT(c.userJWT); }) as (itemValue: string) => void } > @@ -87,9 +81,7 @@ export const SignInScreen = (): React.JSX.Element => { })} - - - + diff --git a/yarn.lock b/yarn.lock index e10022e..d3dbdb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1228,6 +1228,26 @@ xcode "^3.0.1" xml2js "0.6.0" +"@expo/config-plugins@~9.0.17": + version "9.0.17" + resolved "https://registry.yarnpkg.com/@expo/config-plugins/-/config-plugins-9.0.17.tgz#c997072209129b9f9616efa3533314b889cfd788" + integrity sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg== + dependencies: + "@expo/config-types" "^52.0.5" + "@expo/json-file" "~9.0.2" + "@expo/plist" "^0.2.2" + "@expo/sdk-runtime-versions" "^1.0.0" + chalk "^4.1.2" + debug "^4.3.5" + getenv "^1.0.0" + glob "^10.4.2" + resolve-from "^5.0.0" + semver "^7.5.4" + slash "^3.0.0" + slugify "^1.6.6" + xcode "^3.0.1" + xml2js "0.6.0" + "@expo/config-types@^47.0.0": version "47.0.0" resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-47.0.0.tgz#99eeabe0bba7a776e0f252b78beb0c574692c38d" @@ -1238,6 +1258,30 @@ resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.3.tgz#511f2f868172c93abeac7183beeb921dc72d6e1e" integrity sha512-muxvuARmbysH5OGaiBRlh1Y6vfdmL56JtpXxB+y2Hfhu0ezG1U4FjZYBIacthckZPvnDCcP3xIu1R+eTo7/QFA== +"@expo/config-types@^52.0.5": + version "52.0.5" + resolved "https://registry.yarnpkg.com/@expo/config-types/-/config-types-52.0.5.tgz#e10a226990dd903a4e3db5992ffb3015adf13f38" + integrity sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA== + +"@expo/config@~10.0.11": + version "10.0.11" + resolved "https://registry.yarnpkg.com/@expo/config/-/config-10.0.11.tgz#5371ccb3b08ece4c174d5d7009d61e928e6925b0" + integrity sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww== + dependencies: + "@babel/code-frame" "~7.10.4" + "@expo/config-plugins" "~9.0.17" + "@expo/config-types" "^52.0.5" + "@expo/json-file" "^9.0.2" + deepmerge "^4.3.1" + getenv "^1.0.0" + glob "^10.4.2" + require-from-string "^2.0.2" + resolve-from "^5.0.0" + resolve-workspace-root "^2.0.0" + semver "^7.6.0" + slugify "^1.3.4" + sucrase "3.35.0" + "@expo/config@~10.0.8": version "10.0.8" resolved "https://registry.yarnpkg.com/@expo/config/-/config-10.0.8.tgz#c94cf98328d2ec38c9da80ec68d252539cd6eb2d" @@ -1384,6 +1428,23 @@ json5 "^2.2.3" write-file-atomic "^2.3.0" +"@expo/json-file@^9.0.2": + version "9.1.5" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.1.5.tgz#7d7b2dc4990dc2c2de69a571191aba984b7fb7ed" + integrity sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA== + dependencies: + "@babel/code-frame" "~7.10.4" + json5 "^2.2.3" + +"@expo/json-file@~9.0.2": + version "9.0.2" + resolved "https://registry.yarnpkg.com/@expo/json-file/-/json-file-9.0.2.tgz#ec508c2ad17490e0c664c9d7e2ae0ce65915d3ed" + integrity sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw== + dependencies: + "@babel/code-frame" "~7.10.4" + json5 "^2.2.3" + write-file-atomic "^2.3.0" + "@expo/metro-config@0.19.9", "@expo/metro-config@~0.19.9": version "0.19.9" resolved "https://registry.yarnpkg.com/@expo/metro-config/-/metro-config-0.19.9.tgz#f020a2523cecf90e4f2a833386a88e07f6d004f8" @@ -1452,6 +1513,15 @@ base64-js "^1.2.3" xmlbuilder "^14.0.0" +"@expo/plist@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@expo/plist/-/plist-0.2.2.tgz#2563b71b4aa78dc9dbc34cc3d2e1011e994bc9cd" + integrity sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g== + dependencies: + "@xmldom/xmldom" "~0.7.7" + base64-js "^1.2.3" + xmlbuilder "^14.0.0" + "@expo/prebuild-config@5.0.7": version "5.0.7" resolved "https://registry.yarnpkg.com/@expo/prebuild-config/-/prebuild-config-5.0.7.tgz#4658b66126c4d32c7b6302571e458a71811b07aa" @@ -3062,6 +3132,16 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv@8.11.0: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + anser@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" @@ -4373,6 +4453,39 @@ expo-constants@~17.0.4: "@expo/config" "~10.0.8" "@expo/env" "~0.4.1" +expo-dev-client@~5.0.20: + version "5.0.20" + resolved "https://registry.yarnpkg.com/expo-dev-client/-/expo-dev-client-5.0.20.tgz#349a6251d1d63c3142ad5232be653038b5c6cf15" + integrity sha512-bLNkHdU7V3I4UefgJbJnIDUBUL0LxIal/xYEx9BbgDd3B7wgQKY//+BpPIxBOKCQ22lkyiHY8y9tLhO903sAgg== + dependencies: + expo-dev-launcher "5.0.35" + expo-dev-menu "6.0.25" + expo-dev-menu-interface "1.9.3" + expo-manifests "~0.15.8" + expo-updates-interface "~1.0.0" + +expo-dev-launcher@5.0.35: + version "5.0.35" + resolved "https://registry.yarnpkg.com/expo-dev-launcher/-/expo-dev-launcher-5.0.35.tgz#098004658ccb9a55f4170427eb1a35eaf42cea17" + integrity sha512-hEQr0ZREnUMxZ6wtQgfK1lzYnbb0zar3HqYZhmANzXmE6UEPbQ4GByLzhpfz/d+xxdBVQZsrHdtiV28KPG2sog== + dependencies: + ajv "8.11.0" + expo-dev-menu "6.0.25" + expo-manifests "~0.15.8" + resolve-from "^5.0.0" + +expo-dev-menu-interface@1.9.3: + version "1.9.3" + resolved "https://registry.yarnpkg.com/expo-dev-menu-interface/-/expo-dev-menu-interface-1.9.3.tgz#5dc618e498b286a50a9272a8bc71969b6db54e23" + integrity sha512-KY/dWTBE1l47i9V366JN5rC6YIdOc9hz8yAmZzkl5DrPia5l3M2WIjtnpHC9zUkNjiSiG2urYoOAq4H/uLdmyg== + +expo-dev-menu@6.0.25: + version "6.0.25" + resolved "https://registry.yarnpkg.com/expo-dev-menu/-/expo-dev-menu-6.0.25.tgz#72b4607b33d0d6a3823561b1dfe1759a02a86e4a" + integrity sha512-K2m4z/I+CPWbMtHlDzU68lHaQs52De0v5gbsjAmA5ig8FrYh4MKZvPxSVANaiKENzgmtglu8qaFh7ua9Gt2TfA== + dependencies: + expo-dev-menu-interface "1.9.3" + expo-file-system@~18.0.7: version "18.0.7" resolved "https://registry.yarnpkg.com/expo-file-system/-/expo-file-system-18.0.7.tgz#218792dc0aeb7e0976a7f8f412a5d7de09b39610" @@ -4387,6 +4500,11 @@ expo-font@~13.0.3: dependencies: fontfaceobserver "^2.1.0" +expo-json-utils@~0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/expo-json-utils/-/expo-json-utils-0.14.0.tgz#ad3cbbcb4fb22e4d23bf9fb19b611e36758861d2" + integrity sha512-xjGfK9dL0B1wLnOqNkX0jM9p48Y0I5xEPzHude28LY67UmamUyAACkqhZGaPClyPNfdzczk7Ej6WaRMT3HfXvw== + expo-keep-awake@~14.0.2: version "14.0.2" resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-14.0.2.tgz#124a729df43c87994631f51d5b1b5093d58e6c80" @@ -4400,6 +4518,14 @@ expo-linking@~7.0.4: expo-constants "~17.0.4" invariant "^2.2.4" +expo-manifests@~0.15.8: + version "0.15.8" + resolved "https://registry.yarnpkg.com/expo-manifests/-/expo-manifests-0.15.8.tgz#15e7b7b99d764b40ca3e3f859a126c856e2d6206" + integrity sha512-VuIyaMfRfLZeETNsRohqhy1l7iZ7I+HKMPfZXVL2Yn17TT0WkOhZoq1DzYwPbOHPgp1Uk6phNa86EyaHrD2DLw== + dependencies: + "@expo/config" "~10.0.11" + expo-json-utils "~0.14.0" + expo-modules-autolinking@2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/expo-modules-autolinking/-/expo-modules-autolinking-2.0.7.tgz#fc40ba7505f42f971253ea20a927693f2c123a56" @@ -4449,6 +4575,11 @@ expo-splash-screen@~0.29.21: dependencies: "@expo/prebuild-config" "^8.0.25" +expo-updates-interface@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/expo-updates-interface/-/expo-updates-interface-1.0.0.tgz#b98c66b800d29561c62409556948b2af3d5316e5" + integrity sha512-93oWtvULJOj+Pp+N/lpTcFfuREX1wNeHtp7Lwn8EbzYYmdn37MvZU3TPW2tYYCZuhzmKEXnUblYcruYoDu7IrQ== + expo@^52.0.27: version "52.0.27" resolved "https://registry.yarnpkg.com/expo/-/expo-52.0.27.tgz#9eeceda4990ee5a78a66d3f2c26122118ba9454c" @@ -4483,7 +4614,7 @@ fast-base64-decode@^1.0.0: resolved "https://registry.yarnpkg.com/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz#b434a0dd7d92b12b43f26819300d2dafb83ee418" integrity sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q== -fast-deep-equal@^3.1.3: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -5778,6 +5909,11 @@ json-schema-to-ts@3.1.0: "@babel/runtime" "^7.18.3" ts-algebra "^2.0.0" +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify@^1.0.2: version "1.2.1" resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.2.1.tgz#addb683c2b78014d0b78d704c2fcbdf0695a60e2" @@ -6053,6 +6189,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +magicbell-js@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/magicbell-js/-/magicbell-js-1.4.0.tgz#8a060a6f904d1fd0469689a60b51c11bdc9ff613" + integrity sha512-8eJes4+HRfSMr8IrkBV02DMP9TDMdKHeUms2g5z6mz2jS06fO+P+sxSjX4Cy5eJh3lvWjjMmT5mHRiKxuvxTyQ== + dependencies: + zod "3.22.0" + magicbell@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/magicbell/-/magicbell-4.1.0.tgz#3a713fa3f2ff2663d56081c9b3c88175c39c0078" @@ -7058,7 +7201,7 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -punycode@^2.1.1: +punycode@^2.1.0, punycode@^2.1.1: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== @@ -8262,6 +8405,13 @@ update-browserslist-db@^1.1.1: escalade "^3.2.0" picocolors "^1.1.1" +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + url-join@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" @@ -8583,6 +8733,11 @@ yocto-queue@^0.1.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== +zod@3.22.0: + version "3.22.0" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.0.tgz#2478211a9bf477eb2d7d2ce031b5f8ff0d596407" + integrity sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q== + zustand@^4.5.2: version "4.5.5" resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.5.tgz#f8c713041543715ec81a2adda0610e1dc82d4ad1"