Bluesky app fork with some witchin' additions 💫

[APP-786] Native notifications (#1095)

* move `notifee.ts` to notifications folder

* install expo notifications

* add UIBackgroundMode `remote-notifications` to app.json

* fix notifee import in Debug.tsx

* add `google-services.json`

* add `development-device` class to eas.json

* Add `notifications.ts` for native notification handling

* send push token to server

* update `@atproto/api`

* fix putting notif token to server

* fix how push token is uploaded

* fix lint

* enable debug appview proxy header on all platforms

* setup `notifications.ts` to work with app view notifs

* clean up notification handler

* add comments

* update packages to correct versions

* remove notifee

* clean up code a lil

* rename push token endpoint

* remove unnecessary comments

* fix comments

* Remove old background scheduler

* Fixes to push notifications API use

* Bump @atproto/api@0.6.6

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>

authored by Ansh Paul Frazee and committed by GitHub 8ab5eb65 32b96489

+1
.eslintrc.js
··· 17 17 '.husky', 18 18 'patches', 19 19 '*.html', 20 + 'bskyweb', 20 21 ], 21 22 overrides: [ 22 23 {
+4 -1
.gitignore
··· 96 96 97 97 # environment variables 98 98 .env 99 - .env.* 99 + .env.* 100 + 101 + # Firebase (Android) Google services 102 + google-services.json
+2 -6
app.json
··· 25 25 }, 26 26 "infoPlist": { 27 27 "UIBackgroundModes": [ 28 - "fetch", 29 - "processing" 30 - ], 31 - "BGTaskSchedulerPermittedIdentifiers": [ 32 - "com.transistorsoft.fetch" 28 + "remote-notification" 33 29 ], 34 30 "NSCameraUsageDescription": "Used for profile pictures, posts, and other kinds of content.", 35 31 "NSMicrophoneUsageDescription": "Used for posts and other kinds of content.", ··· 48 44 "foregroundImage": "./assets/adaptive-icon.png", 49 45 "backgroundColor": "#ffffff" 50 46 }, 47 + "googleServicesFile": "./google-services.json", 51 48 "package": "xyz.blueskyweb.app", 52 49 "intentFilters": [ 53 50 { ··· 73 70 }, 74 71 "plugins": [ 75 72 "expo-localization", 76 - "react-native-background-fetch", 77 73 "sentry-expo", 78 74 [ 79 75 "expo-build-properties",
+11 -3
eas.json
··· 9 9 "distribution": "internal", 10 10 "ios": { 11 11 "simulator": true, 12 - "resourceClass": "medium" 12 + "resourceClass": "large" 13 + }, 14 + "channel": "development" 15 + }, 16 + "development-device": { 17 + "developmentClient": true, 18 + "distribution": "internal", 19 + "ios": { 20 + "resourceClass": "large" 13 21 }, 14 22 "channel": "development" 15 23 }, 16 24 "preview": { 17 25 "distribution": "internal", 18 26 "ios": { 19 - "resourceClass": "medium" 27 + "resourceClass": "large" 20 28 }, 21 29 "channel": "preview" 22 30 }, 23 31 "production": { 24 32 "ios": { 25 - "resourceClass": "medium" 33 + "resourceClass": "large" 26 34 }, 27 35 "channel": "production" 28 36 }
+2 -3
package.json
··· 24 24 "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" 25 25 }, 26 26 "dependencies": { 27 - "@atproto/api": "^0.6.5", 27 + "@atproto/api": "^0.6.6", 28 28 "@bam.tech/react-native-image-resizer": "^3.0.4", 29 29 "@braintree/sanitize-url": "^6.0.2", 30 30 "@expo/html-elements": "^0.4.2", ··· 36 36 "@gorhom/bottom-sheet": "^4.4.7", 37 37 "@mattermost/react-native-paste-input": "^0.6.4", 38 38 "@miblanchard/react-native-slider": "^2.2.0", 39 - "@notifee/react-native": "^7.4.0", 40 39 "@react-native-async-storage/async-storage": "^1.17.6", 41 40 "@react-native-camera-roll/camera-roll": "^5.2.2", 42 41 "@react-native-clipboard/clipboard": "^1.10.0", ··· 83 82 "expo-image-picker": "^14.1.1", 84 83 "expo-localization": "~14.1.1", 85 84 "expo-media-library": "~15.2.3", 85 + "expo-notifications": "~0.18.1", 86 86 "expo-sharing": "~11.2.2", 87 87 "expo-splash-screen": "~0.18.2", 88 88 "expo-status-bar": "~1.4.4", ··· 114 114 "react-dom": "^18.2.0", 115 115 "react-native": "0.71.8", 116 116 "react-native-appstate-hook": "^1.0.6", 117 - "react-native-background-fetch": "^4.1.8", 118 117 "react-native-draggable-flatlist": "^4.0.1", 119 118 "react-native-drawer-layout": "^3.2.0", 120 119 "react-native-fs": "^2.20.0",
+2 -2
src/App.native.tsx
··· 12 12 import * as view from './view/index' 13 13 import {RootStoreModel, setupState, RootStoreProvider} from './state' 14 14 import {Shell} from './view/shell' 15 - import * as notifee from 'lib/notifee' 15 + import * as notifications from 'lib/notifications/notifications' 16 16 import * as analytics from 'lib/analytics/analytics' 17 17 import * as Toast from './view/com/util/Toast' 18 18 import {handleLink} from './Navigation' ··· 30 30 setupState().then(store => { 31 31 setRootStore(store) 32 32 analytics.init(store) 33 - notifee.init(store) 33 + notifications.init(store) 34 34 SplashScreen.hideAsync() 35 35 Linking.getInitialURL().then((url: string | null) => { 36 36 if (url) {
+20 -19
src/lib/api/debug-appview-proxy-header.ts
··· 8 8 * version of the app. 9 9 */ 10 10 11 - import {useState, useCallback} from 'react' 11 + import {useState, useCallback, useEffect} from 'react' 12 12 import {BskyAgent} from '@atproto/api' 13 13 import {isWeb} from 'platform/detection' 14 + import * as Storage from 'lib/storage' 14 15 15 16 export function useDebugHeaderSetting(agent: BskyAgent): [boolean, () => void] { 16 - const [enabled, setEnabled] = useState<boolean>(isEnabled()) 17 + const [enabled, setEnabled] = useState<boolean>(false) 17 18 18 - const toggle = useCallback(() => { 19 - if (!isWeb || typeof window === 'undefined') { 20 - return 19 + useEffect(() => { 20 + async function check() { 21 + if (await isEnabled()) { 22 + setEnabled(true) 23 + } 21 24 } 25 + check() 26 + }, []) 27 + 28 + const toggle = useCallback(() => { 22 29 if (!enabled) { 23 - localStorage.setItem('set-header-x-appview-proxy', 'yes') 30 + Storage.saveString('set-header-x-appview-proxy', 'yes') 24 31 agent.api.xrpc.setHeader('x-appview-proxy', 'true') 25 32 setEnabled(true) 26 33 } else { 27 - localStorage.removeItem('set-header-x-appview-proxy') 34 + Storage.remove('set-header-x-appview-proxy') 28 35 agent.api.xrpc.unsetHeader('x-appview-proxy') 29 36 setEnabled(false) 30 37 } ··· 34 41 } 35 42 36 43 export function setDebugHeader(agent: BskyAgent, enabled: boolean) { 37 - if (!isWeb || typeof window === 'undefined') { 38 - return 39 - } 40 44 if (enabled) { 41 - localStorage.setItem('set-header-x-appview-proxy', 'yes') 45 + Storage.saveString('set-header-x-appview-proxy', 'yes') 42 46 agent.api.xrpc.setHeader('x-appview-proxy', 'true') 43 47 } else { 44 - localStorage.removeItem('set-header-x-appview-proxy') 48 + Storage.remove('set-header-x-appview-proxy') 45 49 agent.api.xrpc.unsetHeader('x-appview-proxy') 46 50 } 47 51 } 48 52 49 - export function applyDebugHeader(agent: BskyAgent) { 53 + export async function applyDebugHeader(agent: BskyAgent) { 50 54 if (!isWeb) { 51 55 return 52 56 } 53 - if (isEnabled()) { 57 + if (await isEnabled()) { 54 58 agent.api.xrpc.setHeader('x-appview-proxy', 'true') 55 59 } 56 60 } 57 61 58 - function isEnabled() { 59 - if (!isWeb || typeof window === 'undefined') { 60 - return false 61 - } 62 - return localStorage.getItem('set-header-x-appview-proxy') === 'yes' 62 + async function isEnabled() { 63 + return (await Storage.loadString('set-header-x-appview-proxy')) === 'yes' 63 64 }
-18
src/lib/bg-scheduler.ts
··· 1 - import BackgroundFetch, { 2 - BackgroundFetchStatus, 3 - } from 'react-native-background-fetch' 4 - 5 - export function configure( 6 - handler: (taskId: string) => Promise<void>, 7 - timeoutHandler: (taskId: string) => void, 8 - ): Promise<BackgroundFetchStatus> { 9 - return BackgroundFetch.configure( 10 - {minimumFetchInterval: 15}, 11 - handler, 12 - timeoutHandler, 13 - ) 14 - } 15 - 16 - export function finish(taskId: string) { 17 - return BackgroundFetch.finish(taskId) 18 - }
-13
src/lib/bg-scheduler.web.ts
··· 1 - type BackgroundFetchStatus = 0 | 1 | 2 2 - 3 - export async function configure( 4 - _handler: (taskId: string) => Promise<void>, 5 - _timeoutHandler: (taskId: string) => Promise<void>, 6 - ): Promise<BackgroundFetchStatus> { 7 - // TODO 8 - return 0 9 - } 10 - 11 - export function finish(_taskId: string) { 12 - // TODO 13 - }
-82
src/lib/notifee.ts
··· 1 - import notifee, {EventType} from '@notifee/react-native' 2 - import {AppBskyEmbedImages, AtUri} from '@atproto/api' 3 - import {RootStoreModel} from 'state/models/root-store' 4 - import {NotificationsFeedItemModel} from 'state/models/feeds/notifications' 5 - import {enforceLen} from 'lib/strings/helpers' 6 - import {sanitizeDisplayName} from './strings/display-names' 7 - import {resetToTab} from '../Navigation' 8 - 9 - export function init(store: RootStoreModel) { 10 - store.onUnreadNotifications(count => notifee.setBadgeCount(count)) 11 - store.onPushNotification(displayNotificationFromModel) 12 - store.onSessionLoaded(() => { 13 - // request notifications permission once the user has logged in 14 - notifee.requestPermission() 15 - }) 16 - notifee.onForegroundEvent(async ({type}: {type: EventType}) => { 17 - store.log.debug('Notifee foreground event', {type}) 18 - if (type === EventType.PRESS) { 19 - store.log.debug('User pressed a notifee, opening notifications') 20 - resetToTab('NotificationsTab') 21 - } 22 - }) 23 - notifee.onBackgroundEvent(async _e => {}) // notifee requires this but we handle it with onForegroundEvent 24 - } 25 - 26 - export function displayNotification( 27 - title: string, 28 - body?: string, 29 - image?: string, 30 - ) { 31 - const opts: {title: string; body?: string; ios?: any} = {title} 32 - if (body) { 33 - opts.body = enforceLen(body, 70, true) 34 - } 35 - if (image) { 36 - opts.ios = { 37 - attachments: [{url: image}], 38 - } 39 - } 40 - return notifee.displayNotification(opts) 41 - } 42 - 43 - export function displayNotificationFromModel( 44 - notification: NotificationsFeedItemModel, 45 - ) { 46 - let author = sanitizeDisplayName( 47 - notification.author.displayName || notification.author.handle, 48 - ) 49 - let title: string 50 - let body: string = '' 51 - if (notification.isLike) { 52 - title = `${author} liked your post` 53 - body = notification.additionalPost?.thread?.postRecord?.text || '' 54 - } else if (notification.isRepost) { 55 - title = `${author} reposted your post` 56 - body = notification.additionalPost?.thread?.postRecord?.text || '' 57 - } else if (notification.isMention) { 58 - title = `${author} mentioned you` 59 - body = notification.additionalPost?.thread?.postRecord?.text || '' 60 - } else if (notification.isReply) { 61 - title = `${author} replied to your post` 62 - body = notification.additionalPost?.thread?.postRecord?.text || '' 63 - } else if (notification.isFollow) { 64 - title = 'New follower!' 65 - body = `${author} has followed you` 66 - } else if (notification.isCustomFeedLike) { 67 - title = `${author} liked your custom feed` 68 - body = `${new AtUri(notification.subjectUri).rkey}` 69 - } else { 70 - return 71 - } 72 - let image 73 - if ( 74 - AppBskyEmbedImages.isView( 75 - notification.additionalPost?.thread?.post.embed, 76 - ) && 77 - notification.additionalPost?.thread?.post.embed.images[0]?.thumb 78 - ) { 79 - image = notification.additionalPost.thread.post.embed.images[0].thumb 80 - } 81 - return displayNotification(title, body, image) 82 - }
+101
src/lib/notifications/notifications.ts
··· 1 + import * as Notifications from 'expo-notifications' 2 + import {RootStoreModel} from '../../state' 3 + import {resetToTab} from '../../Navigation' 4 + import {devicePlatform, isIOS} from 'platform/detection' 5 + 6 + // TODO prod did = did:web:api.bsky.app 7 + 8 + export function init(store: RootStoreModel) { 9 + store.onUnreadNotifications(count => Notifications.setBadgeCountAsync(count)) 10 + 11 + store.onSessionLoaded(async () => { 12 + // request notifications permission once the user has logged in 13 + const perms = await Notifications.getPermissionsAsync() 14 + if (!perms.granted) { 15 + await Notifications.requestPermissionsAsync() 16 + } 17 + 18 + // register the push token with the server 19 + const token = await getPushToken() 20 + if (token) { 21 + try { 22 + await store.agent.api.app.bsky.notification.registerPush({ 23 + serviceDid: 'did:web:api.staging.bsky.dev', 24 + platform: devicePlatform, 25 + token: token.data, 26 + appId: 'xyz.blueskyweb.app', 27 + }) 28 + store.log.debug('Notifications: Sent push token (init)', { 29 + type: token.type, 30 + token: token.data, 31 + }) 32 + } catch (error) { 33 + store.log.error('Notifications: Failed to set push token', error) 34 + } 35 + } 36 + 37 + // listens for new changes to the push token 38 + // In rare situations, a push token may be changed by the push notification service while the app is running. When a token is rolled, the old one becomes invalid and sending notifications to it will fail. A push token listener will let you handle this situation gracefully by registering the new token with your backend right away. 39 + Notifications.addPushTokenListener(async ({data: t, type}) => { 40 + store.log.debug('Notifications: Push token changed', {t, type}) 41 + if (t) { 42 + try { 43 + await store.agent.api.app.bsky.notification.registerPush({ 44 + serviceDid: 'did:web:api.staging.bsky.dev', 45 + platform: devicePlatform, 46 + token: t, 47 + appId: 'xyz.blueskyweb.app', 48 + }) 49 + store.log.debug('Notifications: Sent push token (event)', { 50 + type, 51 + token: t, 52 + }) 53 + } catch (error) { 54 + store.log.error('Notifications: Failed to set push token', error) 55 + } 56 + } 57 + }) 58 + }) 59 + 60 + // handle notifications that are tapped on, regardless of whether the app is in the foreground or background 61 + Notifications.addNotificationReceivedListener(event => { 62 + store.log.debug('Notifications: received', event) 63 + if (event.request.trigger.type === 'push') { 64 + let payload 65 + if (isIOS) { 66 + payload = event.request.trigger.payload 67 + } else { 68 + // TODO: handle android payload deeplink 69 + } 70 + if (payload) { 71 + store.log.debug('Notifications: received payload', payload) 72 + // TODO: deeplink notif here 73 + } 74 + } 75 + }) 76 + 77 + const sub = Notifications.addNotificationResponseReceivedListener( 78 + response => { 79 + store.log.debug( 80 + 'Notifications: response received', 81 + response.actionIdentifier, 82 + ) 83 + if ( 84 + response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER 85 + ) { 86 + store.log.debug( 87 + 'User pressed a notification, opening notifications tab', 88 + ) 89 + resetToTab('NotificationsTab') 90 + } 91 + }, 92 + ) 93 + 94 + return () => { 95 + sub.remove() 96 + } 97 + } 98 + 99 + export function getPushToken() { 100 + return Notifications.getDevicePushTokenAsync() 101 + }
+1
src/platform/detection.ts
··· 5 5 export const isIOS = Platform.OS === 'ios' 6 6 export const isAndroid = Platform.OS === 'android' 7 7 export const isNative = isIOS || isAndroid 8 + export const devicePlatform = isIOS ? 'ios' : isAndroid ? 'android' : 'web' 8 9 export const isWeb = !isNative 9 10 export const isMobileWebMediaQuery = 'only screen and (max-width: 1230px)' 10 11 export const isMobileWeb =
-30
src/state/models/feeds/notifications.ts
··· 478 478 } 479 479 } 480 480 481 - /** 482 - * Used in background fetch to trigger notifications 483 - */ 484 - async getNewMostRecent(): Promise<NotificationsFeedItemModel | undefined> { 485 - let old = this.mostRecentNotificationUri 486 - const res = await this.rootStore.agent.listNotifications({ 487 - limit: 1, 488 - }) 489 - if (!res.data.notifications[0] || old === res.data.notifications[0].uri) { 490 - return 491 - } 492 - this.mostRecentNotificationUri = res.data.notifications[0].uri 493 - const notif = new NotificationsFeedItemModel( 494 - this.rootStore, 495 - 'mostRecent', 496 - res.data.notifications[0], 497 - ) 498 - const addedUri = notif.additionalDataUri 499 - if (addedUri) { 500 - const postsRes = await this.rootStore.agent.app.bsky.feed.getPosts({ 501 - uris: [addedUri], 502 - }) 503 - const post = postsRes.data.posts[0] 504 - notif.setAdditionalData(post) 505 - this.rootStore.posts.set(post.uri, post) 506 - } 507 - const filtered = this._filterNotifications([notif]) 508 - return filtered[0] 509 - } 510 - 511 481 // state transitions 512 482 // = 513 483
-59
src/state/models/root-store.ts
··· 6 6 import {BskyAgent} from '@atproto/api' 7 7 import {createContext, useContext} from 'react' 8 8 import {DeviceEventEmitter, EmitterSubscription} from 'react-native' 9 - import * as BgScheduler from 'lib/bg-scheduler' 10 9 import {z} from 'zod' 11 10 import {isObj, hasProp} from 'lib/type-guards' 12 11 import {LogModel} from './log' ··· 16 15 import {ProfilesCache} from './cache/profiles-view' 17 16 import {PostsCache} from './cache/posts' 18 17 import {LinkMetasCache} from './cache/link-metas' 19 - import {NotificationsFeedItemModel} from './feeds/notifications' 20 18 import {MeModel} from './me' 21 19 import {InvitedUsers} from './invited-users' 22 20 import {PreferencesModel} from './ui/preferences' ··· 61 59 serialize: false, 62 60 hydrate: false, 63 61 }) 64 - this.initBgFetch() 65 62 } 66 63 67 64 setAppInfo(info: AppInfo) { ··· 248 245 } 249 246 emitUnreadNotifications(count: number) { 250 247 DeviceEventEmitter.emit('unread-notifications', count) 251 - } 252 - 253 - // a notification has been queued for push 254 - onPushNotification( 255 - handler: (notif: NotificationsFeedItemModel) => void, 256 - ): EmitterSubscription { 257 - return DeviceEventEmitter.addListener('push-notification', handler) 258 - } 259 - emitPushNotification(notif: NotificationsFeedItemModel) { 260 - DeviceEventEmitter.emit('push-notification', notif) 261 - } 262 - 263 - // background fetch 264 - // = 265 - // - we use this to poll for unread notifications, which is not "ideal" behavior but 266 - // gives us a solution for push-notifications that work against any pds 267 - 268 - initBgFetch() { 269 - // NOTE 270 - // background fetch runs every 15 minutes *at most* and will get slowed down 271 - // based on some heuristics run by iOS, meaning it is not a reliable form of delivery 272 - // -prf 273 - BgScheduler.configure( 274 - this.onBgFetch.bind(this), 275 - this.onBgFetchTimeout.bind(this), 276 - ).then(status => { 277 - this.log.debug(`Background fetch initiated, status: ${status}`) 278 - }) 279 - } 280 - 281 - async onBgFetch(taskId: string) { 282 - this.log.debug(`Background fetch fired for task ${taskId}`) 283 - if (this.session.hasSession) { 284 - const res = await this.agent.countUnreadNotifications() 285 - const hasNewNotifs = this.me.notifications.unreadCount !== res.data.count 286 - this.emitUnreadNotifications(res.data.count) 287 - this.log.debug( 288 - `Background fetch received unread count = ${res.data.count}`, 289 - ) 290 - if (hasNewNotifs) { 291 - this.log.debug( 292 - 'Background fetch detected potentially a new notification', 293 - ) 294 - const mostRecent = await this.me.notifications.getNewMostRecent() 295 - if (mostRecent) { 296 - this.log.debug('Got the notification, triggering a push') 297 - this.emitPushNotification(mostRecent) 298 - } 299 - } 300 - } 301 - BgScheduler.finish(taskId) 302 - } 303 - 304 - onBgFetchTimeout(taskId: string) { 305 - this.log.debug(`Background fetch timed out for task ${taskId}`) 306 - BgScheduler.finish(taskId) 307 248 } 308 249 } 309 250
+1 -6
src/view/screens/Debug.tsx
··· 5 5 import {ThemeProvider, PaletteColorName} from 'lib/ThemeContext' 6 6 import {usePalette} from 'lib/hooks/usePalette' 7 7 import {s} from 'lib/styles' 8 - import {displayNotification} from 'lib/notifee' 9 8 import * as Toast from 'view/com/util/Toast' 10 - 11 9 import {Text} from '../com/util/text/Text' 12 10 import {ViewSelector} from '../com/util/ViewSelector' 13 11 import {EmptyState} from '../com/util/EmptyState' ··· 177 175 178 176 function NotifsView() { 179 177 const triggerPush = () => { 180 - displayNotification( 181 - 'Paul Frazee liked your post', 182 - "Hello world! This is a test of the notifications card. The text is long to see how that's handled.", 183 - ) 178 + // TODO: implement local notification for testing 184 179 } 185 180 const triggerToast = () => { 186 181 Toast.show('The task has been completed')
+1 -1
src/view/screens/Settings.tsx
··· 505 505 System log 506 506 </Text> 507 507 </TouchableOpacity> 508 - {isDesktopWeb ? ( 508 + {isDesktopWeb || __DEV__ ? ( 509 509 <ToggleButton 510 510 type="default-light" 511 511 label="Experiment: Use AppView Proxy"
+1 -1
tsconfig.check.json
··· 1 1 { 2 2 "extends": "./tsconfig.json", 3 - "exclude": ["__e2e__", "dist"], 3 + "include": ["src"] 4 4 }
+106 -16
yarn.lock
··· 40 40 tlds "^1.234.0" 41 41 typed-emitter "^2.1.0" 42 42 43 - "@atproto/api@^0.6.5": 44 - version "0.6.5" 45 - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.5.tgz#496a011b7e8fbf2af32a30cec07a021aa6ef3f4b" 46 - integrity sha512-u6NVkYpdUU5jKGxio2FIRmok0LL+eqqMzihm9LDfydZ4Pi4NqfrOrWw0H1WA7zO3vH9AaxnLSMTwSEAkRRb2FA== 43 + "@atproto/api@^0.6.6": 44 + version "0.6.6" 45 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.6.tgz#c1bfdb6bc7dee9cdba1901cde0081c2d422d7c29" 46 + integrity sha512-j+yNTjllVxuTc4bAegghTopju7MdhczLXWvWIli40uXwCzQ3JjS1mFr/47eETtysib2phWYQvfhtCrqQq6AAig== 47 47 dependencies: 48 48 "@atproto/common-web" "*" 49 49 "@atproto/uri" "*" ··· 3089 3089 semver "7.3.2" 3090 3090 tempy "0.3.0" 3091 3091 3092 - "@expo/image-utils@0.3.23": 3092 + "@expo/image-utils@0.3.23", "@expo/image-utils@^0.3.18": 3093 3093 version "0.3.23" 3094 3094 resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.23.tgz#f14fd7e1f5ff6f8e4911a41e27dd274470665c3f" 3095 3095 integrity sha512-nhUVvW0TrRE4jtWzHQl8TR4ox7kcmrc2I0itaeJGjxF5A54uk7avgA0wRt7jP1rdvqQo1Ke1lXyLYREdhN9tPw== ··· 3361 3361 version "1.2.1" 3362 3362 resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" 3363 3363 integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== 3364 + 3365 + "@ide/backoff@^1.0.0": 3366 + version "1.0.0" 3367 + resolved "https://registry.yarnpkg.com/@ide/backoff/-/backoff-1.0.0.tgz#466842c25bd4a4833e0642fab41ccff064010176" 3368 + integrity sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g== 3364 3369 3365 3370 "@ipld/car@^3.2.3": 3366 3371 version "3.2.4" ··· 3982 3987 "@nodelib/fs.scandir" "2.1.5" 3983 3988 fastq "^1.6.0" 3984 3989 3985 - "@notifee/react-native@^7.4.0": 3986 - version "7.8.0" 3987 - resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.8.0.tgz#2990883753990f3585aa0cb5becc5cbdbcd87a43" 3988 - integrity sha512-sx8h62U4FrR4pqlbN1rkgPsdamDt9Tad0zgfO6VtP6rNJq/78k8nxUnh0xIX3WPDcCV8KAzdYCE7+UNvhF1CpQ== 3989 - 3990 3990 "@npmcli/fs@^1.0.0": 3991 3991 version "1.1.1" 3992 3992 resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257" ··· 7290 7290 minimalistic-assert "^1.0.0" 7291 7291 safer-buffer "^2.1.0" 7292 7292 7293 + assert@^2.0.0: 7294 + version "2.0.0" 7295 + resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32" 7296 + integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A== 7297 + dependencies: 7298 + es6-object-assign "^1.1.0" 7299 + is-nan "^1.2.1" 7300 + object-is "^1.0.1" 7301 + util "^0.12.0" 7302 + 7293 7303 assign-symbols@^1.0.0: 7294 7304 version "1.0.0" 7295 7305 resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" ··· 7664 7674 babel-plugin-macros "^3.1.0" 7665 7675 babel-plugin-transform-react-remove-prop-types "^0.4.24" 7666 7676 7677 + badgin@^1.1.5: 7678 + version "1.2.3" 7679 + resolved "https://registry.yarnpkg.com/badgin/-/badgin-1.2.3.tgz#994b5f519827d7d5422224825b2c8faea2bc43ad" 7680 + integrity sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw== 7681 + 7667 7682 balanced-match@^1.0.0: 7668 7683 version "1.0.2" 7669 7684 resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" ··· 9772 9787 is-date-object "^1.0.1" 9773 9788 is-symbol "^1.0.2" 9774 9789 9790 + es6-object-assign@^1.1.0: 9791 + version "1.1.0" 9792 + resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" 9793 + integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw== 9794 + 9775 9795 escalade@^3.1.1: 9776 9796 version "3.1.1" 9777 9797 resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" ··· 10252 10272 jest-message-util "^29.5.0" 10253 10273 jest-util "^29.5.0" 10254 10274 10255 - expo-application@~5.1.1: 10275 + expo-application@~5.1.0, expo-application@~5.1.1: 10256 10276 version "5.1.1" 10257 10277 resolved "https://registry.yarnpkg.com/expo-application/-/expo-application-5.1.1.tgz#5206bf0cf89cb0e32d1f5037a0481e5c86b951ab" 10258 10278 integrity sha512-aDatTcTTCdTbHw8h4/Tq2ilc6InM5ntF9xWCJdOcnUEcglxxGphVI/lzJKBaBF6mJECA8mEOjpVg2EGxOctTwg== ··· 10425 10445 compare-versions "^3.4.0" 10426 10446 invariant "^2.2.4" 10427 10447 10448 + expo-notifications@~0.18.1: 10449 + version "0.18.1" 10450 + resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.18.1.tgz#c726bee7b6691d5f154b874afeda3b4561571cfc" 10451 + integrity sha512-lOEiuPE6ubkS5u7Nj/57gkmUGD/MxsRTC6bg9SGJqXIitBQZk3Tmv9y8bjTrn71n7DsrH8K7xCZTbVwr+kLQGg== 10452 + dependencies: 10453 + "@expo/image-utils" "^0.3.18" 10454 + "@ide/backoff" "^1.0.0" 10455 + abort-controller "^3.0.0" 10456 + assert "^2.0.0" 10457 + badgin "^1.1.5" 10458 + expo-application "~5.1.0" 10459 + expo-constants "~14.2.0" 10460 + fs-extra "^9.1.0" 10461 + uuid "^3.4.0" 10462 + 10428 10463 expo-pwa@0.0.125: 10429 10464 version "0.0.125" 10430 10465 resolved "https://registry.yarnpkg.com/expo-pwa/-/expo-pwa-0.0.125.tgz#fb5a66f21e7c9a51cdfa76d692b48bd116e6e002" ··· 11951 11986 dependencies: 11952 11987 kind-of "^6.0.0" 11953 11988 11989 + is-arguments@^1.0.4: 11990 + version "1.1.1" 11991 + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" 11992 + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== 11993 + dependencies: 11994 + call-bind "^1.0.2" 11995 + has-tostringtag "^1.0.0" 11996 + 11954 11997 is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: 11955 11998 version "3.0.2" 11956 11999 resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" ··· 12121 12164 resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" 12122 12165 integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== 12123 12166 12167 + is-generator-function@^1.0.7: 12168 + version "1.0.10" 12169 + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" 12170 + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== 12171 + dependencies: 12172 + has-tostringtag "^1.0.0" 12173 + 12124 12174 is-glob@^2.0.0: 12125 12175 version "2.0.1" 12126 12176 resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" ··· 12152 12202 resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" 12153 12203 integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== 12154 12204 12205 + is-nan@^1.2.1: 12206 + version "1.3.2" 12207 + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" 12208 + integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== 12209 + dependencies: 12210 + call-bind "^1.0.0" 12211 + define-properties "^1.1.3" 12212 + 12155 12213 is-negative-zero@^2.0.2: 12156 12214 version "2.0.2" 12157 12215 resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" ··· 12291 12349 for-each "^0.3.3" 12292 12350 gopd "^1.0.1" 12293 12351 has-tostringtag "^1.0.0" 12352 + 12353 + is-typed-array@^1.1.3: 12354 + version "1.1.12" 12355 + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" 12356 + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== 12357 + dependencies: 12358 + which-typed-array "^1.1.11" 12294 12359 12295 12360 is-typedarray@^1.0.0: 12296 12361 version "1.0.0" ··· 15149 15214 resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" 15150 15215 integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== 15151 15216 15217 + object-is@^1.0.1: 15218 + version "1.1.5" 15219 + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" 15220 + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== 15221 + dependencies: 15222 + call-bind "^1.0.2" 15223 + define-properties "^1.1.3" 15224 + 15152 15225 object-keys@^1.1.1: 15153 15226 version "1.1.1" 15154 15227 resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" ··· 16984 17057 version "1.0.6" 16985 17058 resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06" 16986 17059 integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ== 16987 - 16988 - react-native-background-fetch@^4.1.8: 16989 - version "4.1.10" 16990 - resolved "https://registry.yarnpkg.com/react-native-background-fetch/-/react-native-background-fetch-4.1.10.tgz#12c7e85140af67fb05edb7cd9960e4f09a457797" 16991 - integrity sha512-Ug54vTctZuD/c06ZLk/VyvFdhw/hCVVOHYR5heyMqc6FlT/m9fVhFWyl4uH3JmPCzmWDVR3fO28CzrGpKOrusw== 16992 17060 16993 17061 react-native-codegen@^0.71.5: 16994 17062 version "0.71.5" ··· 19704 19772 has-symbols "^1.0.1" 19705 19773 object.getownpropertydescriptors "^2.1.0" 19706 19774 19775 + util@^0.12.0: 19776 + version "0.12.5" 19777 + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" 19778 + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== 19779 + dependencies: 19780 + inherits "^2.0.3" 19781 + is-arguments "^1.0.4" 19782 + is-generator-function "^1.0.7" 19783 + is-typed-array "^1.1.3" 19784 + which-typed-array "^1.1.2" 19785 + 19707 19786 utila@~0.4: 19708 19787 version "0.4.0" 19709 19788 resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" ··· 20103 20182 version "2.0.1" 20104 20183 resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" 20105 20184 integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== 20185 + 20186 + which-typed-array@^1.1.11, which-typed-array@^1.1.2: 20187 + version "1.1.11" 20188 + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" 20189 + integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== 20190 + dependencies: 20191 + available-typed-arrays "^1.0.5" 20192 + call-bind "^1.0.2" 20193 + for-each "^0.3.3" 20194 + gopd "^1.0.1" 20195 + has-tostringtag "^1.0.0" 20106 20196 20107 20197 which-typed-array@^1.1.9: 20108 20198 version "1.1.9"