diff --git a/apps/native/app/android/app/src/prod/AndroidManifest.xml b/apps/native/app/android/app/src/prod/AndroidManifest.xml index 7fb79e9924d4..afbc625e51d5 100644 --- a/apps/native/app/android/app/src/prod/AndroidManifest.xml +++ b/apps/native/app/android/app/src/prod/AndroidManifest.xml @@ -26,6 +26,18 @@ + + + + + + + + + + + + diff --git a/apps/native/app/ios/IslandApp/IslandApp.entitlements b/apps/native/app/ios/IslandApp/IslandApp.entitlements index 710049617b26..2cc8fb471c65 100644 --- a/apps/native/app/ios/IslandApp/IslandApp.entitlements +++ b/apps/native/app/ios/IslandApp/IslandApp.entitlements @@ -7,6 +7,7 @@ com.apple.developer.associated-domains webcredentials:island.is + applinks:island.is keychain-access-groups diff --git a/apps/native/app/package.json b/apps/native/app/package.json index b1604aa07f38..fdcf1cede4ab 100644 --- a/apps/native/app/package.json +++ b/apps/native/app/package.json @@ -57,6 +57,7 @@ "expo": "51.0.25", "expo-file-system": "17.0.1", "expo-haptics": "13.0.1", + "expo-linking": "6.3.1", "expo-local-authentication": "14.0.1", "expo-notifications": "0.28.9", "intl": "1.2.5", diff --git a/apps/native/app/src/hooks/use-deep-link-handling.ts b/apps/native/app/src/hooks/use-deep-link-handling.ts new file mode 100644 index 000000000000..aab6a9dbe2a5 --- /dev/null +++ b/apps/native/app/src/hooks/use-deep-link-handling.ts @@ -0,0 +1,73 @@ +import messaging, { + FirebaseMessagingTypes, +} from '@react-native-firebase/messaging' +import { useCallback, useEffect, useRef, useState } from 'react' +import { useURL } from 'expo-linking' +import { useMarkUserNotificationAsReadMutation } from '../graphql/types/schema' + +import { navigateToUniversalLink } from '../lib/deep-linking' +import { useBrowser } from '../lib/use-browser' +import { useAuthStore } from '../stores/auth-store' + +// Expo-style notification hook wrapping firebase. +function useLastNotificationResponse() { + const [lastNotificationResponse, setLastNotificationResponse] = + useState(null) + + useEffect(() => { + messaging() + .getInitialNotification() + .then((remoteMessage) => { + if (remoteMessage) { + setLastNotificationResponse(remoteMessage) + } + }) + + // Return the unsubscribe function as a useEffect destructor. + return messaging().onNotificationOpenedApp((remoteMessage) => { + setLastNotificationResponse(remoteMessage) + }) + }, []) + + return lastNotificationResponse +} + +export function useDeepLinkHandling() { + const url = useURL() + const notification = useLastNotificationResponse() + const [markUserNotificationAsRead] = useMarkUserNotificationAsReadMutation() + const lockScreenActivatedAt = useAuthStore( + ({ lockScreenActivatedAt }) => lockScreenActivatedAt, + ) + + const lastUrl = useRef(null) + const { openBrowser } = useBrowser() + + const handleUrl = useCallback( + (url?: string | null) => { + if (!url || lastUrl.current === url || lockScreenActivatedAt) { + return false + } + lastUrl.current = url + + navigateToUniversalLink({ link: url, openBrowser }) + return true + }, + [openBrowser, lastUrl, lockScreenActivatedAt], + ) + + useEffect(() => { + handleUrl(url) + }, [url, handleUrl]) + + useEffect(() => { + const url = notification?.data?.clickActionUrl + const wasHandled = handleUrl(url) + if (wasHandled && notification?.data?.notificationId) { + // Mark notification as read and seen + void markUserNotificationAsRead({ + variables: { id: Number(notification.data.notificationId) }, + }) + } + }, [notification, handleUrl, markUserNotificationAsRead]) +} diff --git a/apps/native/app/src/index.tsx b/apps/native/app/src/index.tsx index e01b5ee33d96..586fedb0f9a9 100644 --- a/apps/native/app/src/index.tsx +++ b/apps/native/app/src/index.tsx @@ -8,7 +8,6 @@ import { registerAllComponents } from './utils/lifecycle/setup-components' import { setupDevMenu } from './utils/lifecycle/setup-dev-menu' import { setupEventHandlers } from './utils/lifecycle/setup-event-handlers' import { setupGlobals } from './utils/lifecycle/setup-globals' -import { setupNotifications } from './utils/lifecycle/setup-notifications' import { setupRoutes } from './utils/lifecycle/setup-routes' import { performanceMetricsAppLaunched } from './utils/performance-metrics' @@ -25,9 +24,6 @@ async function startApp() { // Setup app routing layer setupRoutes() - // Setup notifications - setupNotifications() - // Initialize Apollo client. This must be done before registering components await initializeApolloClient() diff --git a/apps/native/app/src/lib/deep-linking.ts b/apps/native/app/src/lib/deep-linking.ts index 9bb7670d3af0..1857145c1fd0 100644 --- a/apps/native/app/src/lib/deep-linking.ts +++ b/apps/native/app/src/lib/deep-linking.ts @@ -186,16 +186,18 @@ export function navigateTo(url: string, extraProps: any = {}) { } /** - * Navigate to a notification ClickActionUrl, if our mapping does not return a valid screen within the app - open a webview. + * Navigate to a specific universal link, if our mapping does not return a valid screen within the app - open a webview. */ -export function navigateToNotification({ +export function navigateToUniversalLink({ link, componentId, + openBrowser = openNativeBrowser, }: { // url to navigate to link?: NotificationMessage['link']['url'] // componentId to open web browser in componentId?: string + openBrowser?: (link: string, componentId?: string) => void }) { // If no link do nothing if (!link) return @@ -216,13 +218,14 @@ export function navigateToNotification({ }, }) } - // TODO: When navigating to a link from notification works, implement a way to use useBrowser.openBrowser here - openNativeBrowser(link, componentId ?? ComponentRegistry.HomeScreen) + + openBrowser(link, componentId ?? ComponentRegistry.HomeScreen) } // Map between notification link and app screen const urlMapping: { [key: string]: string } = { '/minarsidur/postholf/:id': '/inbox/:id', + '/minarsidur/postholf': '/inbox', '/minarsidur/min-gogn/stillingar': '/settings', '/minarsidur/skirteini': '/wallet', '/minarsidur/skirteini/tjodskra/vegabref/:id': '/walletpassport/:id', diff --git a/apps/native/app/src/screens/home/home.tsx b/apps/native/app/src/screens/home/home.tsx index d9d375c872b3..07106e266c41 100644 --- a/apps/native/app/src/screens/home/home.tsx +++ b/apps/native/app/src/screens/home/home.tsx @@ -20,12 +20,21 @@ import { BottomTabsIndicator } from '../../components/bottom-tabs-indicator/bott import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' import { useAndroidNotificationPermission } from '../../hooks/use-android-notification-permission' import { useConnectivityIndicator } from '../../hooks/use-connectivity-indicator' +import { useDeepLinkHandling } from '../../hooks/use-deep-link-handling' import { useNotificationsStore } from '../../stores/notifications-store' +import { + preferencesStore, + usePreferencesStore, +} from '../../stores/preferences-store' import { useUiStore } from '../../stores/ui-store' import { isAndroid } from '../../utils/devices' import { getRightButtons } from '../../utils/get-main-root' -import { handleInitialNotification } from '../../utils/lifecycle/setup-notifications' import { testIDs } from '../../utils/test-ids' +import { + AirDiscountModule, + useGetAirDiscountQuery, + validateAirDiscountInitialData, +} from './air-discount-module' import { ApplicationsModule, useListApplicationsQuery, @@ -37,26 +46,17 @@ import { useListDocumentsQuery, validateInboxInitialData, } from './inbox-module' +import { + LicensesModule, + useGetLicensesData, + validateLicensesInitialData, +} from './licenses-module' import { OnboardingModule } from './onboarding-module' import { - VehiclesModule, useListVehiclesQuery, validateVehiclesInitialData, + VehiclesModule, } from './vehicles-module' -import { - preferencesStore, - usePreferencesStore, -} from '../../stores/preferences-store' -import { - AirDiscountModule, - useGetAirDiscountQuery, - validateAirDiscountInitialData, -} from './air-discount-module' -import { - LicensesModule, - validateLicensesInitialData, - useGetLicensesData, -} from './licenses-module' interface ListItem { id: string @@ -150,6 +150,8 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ ({ widgetsInitialised }) => widgetsInitialised, ) + useDeepLinkHandling() + const applicationsRes = useListApplicationsQuery({ skip: !applicationsWidgetEnabled, }) @@ -258,9 +260,6 @@ export const MainHomeScreen: NavigationFunctionComponent = ({ checkUnseen() // Get user locale from server getAndSetLocale() - - // Handle initial notification - handleInitialNotification() }, []) const refetch = useCallback(async () => { diff --git a/apps/native/app/src/screens/notifications/notifications.tsx b/apps/native/app/src/screens/notifications/notifications.tsx index aa54c3fe624a..913c1b6d6d50 100644 --- a/apps/native/app/src/screens/notifications/notifications.tsx +++ b/apps/native/app/src/screens/notifications/notifications.tsx @@ -34,7 +34,7 @@ import { } from '../../graphql/types/schema' import { createNavigationOptionHooks } from '../../hooks/create-navigation-option-hooks' -import { navigateTo, navigateToNotification } from '../../lib/deep-linking' +import { navigateTo, navigateToUniversalLink } from '../../lib/deep-linking' import { useNotificationsStore } from '../../stores/notifications-store' import { createSkeletonArr, @@ -45,6 +45,7 @@ import { testIDs } from '../../utils/test-ids' import settings from '../../assets/icons/settings.png' import inboxRead from '../../assets/icons/inbox-read.png' import emptyIllustrationSrc from '../../assets/illustrations/le-company-s3.png' +import { useBrowser } from '../../lib/use-browser' const LoadingWrapper = styled.View` padding-vertical: ${({ theme }) => theme.spacing[3]}px; @@ -85,6 +86,7 @@ export const NotificationsScreen: NavigationFunctionComponent = ({ componentId, }) => { useNavigationOptions(componentId) + const { openBrowser } = useBrowser() const intl = useIntl() const theme = useTheme() const client = useApolloClient() @@ -147,15 +149,19 @@ export const NotificationsScreen: NavigationFunctionComponent = ({ return data?.userNotifications?.data || [] }, [data, loading]) - const onNotificationPress = useCallback((notification: Notification) => { - // Mark notification as read and seen - void markUserNotificationAsRead({ variables: { id: notification.id } }) + const onNotificationPress = useCallback( + (notification: Notification) => { + // Mark notification as read and seen + void markUserNotificationAsRead({ variables: { id: notification.id } }) - navigateToNotification({ - componentId, - link: notification.message?.link?.url, - }) - }, []) + navigateToUniversalLink({ + componentId, + link: notification.message?.link?.url, + openBrowser, + }) + }, + [markUserNotificationAsRead, componentId, openBrowser], + ) const handleEndReached = async () => { if ( diff --git a/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts b/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts index 4512b2028297..b5db7bf84337 100644 --- a/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts +++ b/apps/native/app/src/utils/lifecycle/setup-event-handlers.ts @@ -28,13 +28,6 @@ let backgroundAppLockTimeout: ReturnType export function setupEventHandlers() { // Listen for url events through iOS and Android's Linking library Linking.addEventListener('url', ({ url }) => { - console.log('URL', url) - Linking.canOpenURL(url).then((supported) => { - if (supported) { - evaluateUrl(url) - } - }) - // Handle Cognito if (/cognito/.test(url)) { const [, hash] = url.split('#') @@ -66,15 +59,6 @@ export function setupEventHandlers() { }) } - // Get initial url and pass to the opener - Linking.getInitialURL() - .then((url) => { - if (url) { - Linking.openURL(url) - } - }) - .catch((err) => console.error('An error occurred in getInitialURL: ', err)) - Navigation.events().registerBottomTabSelectedListener((e) => { uiStore.setState({ unselectedTab: e.unselectedTabIndex, diff --git a/apps/native/app/src/utils/lifecycle/setup-notifications.ts b/apps/native/app/src/utils/lifecycle/setup-notifications.ts deleted file mode 100644 index 363fe1559da2..000000000000 --- a/apps/native/app/src/utils/lifecycle/setup-notifications.ts +++ /dev/null @@ -1,95 +0,0 @@ -import messaging, { - FirebaseMessagingTypes, -} from '@react-native-firebase/messaging' -import { - DEFAULT_ACTION_IDENTIFIER, - Notification, - NotificationResponse, -} from 'expo-notifications' -import { navigateTo, navigateToNotification } from '../../lib/deep-linking' - -export const ACTION_IDENTIFIER_NO_OPERATION = 'NOOP' - -export async function handleNotificationResponse({ - actionIdentifier, - notification, -}: NotificationResponse) { - const link = - notification.request.content.data?.clickActionUrl ?? - notification.request.content.data?.link - - if ( - typeof link === 'string' && - actionIdentifier !== ACTION_IDENTIFIER_NO_OPERATION - ) { - navigateToNotification({ link }) - } else { - navigateTo('/notifications') - } -} - -function mapRemoteMessage( - remoteMessage: FirebaseMessagingTypes.RemoteMessage, -): Notification { - return { - date: remoteMessage.sentTime ?? 0, - request: { - content: { - title: remoteMessage.notification?.title || null, - subtitle: null, - body: remoteMessage.notification?.body || null, - data: { - link: remoteMessage.notification?.android?.link, - ...remoteMessage.data, - }, - sound: 'default', - }, - identifier: remoteMessage.messageId ?? '', - trigger: { - type: 'push', - }, - }, - } -} - -export function setupNotifications() { - // FCMs - - messaging().onNotificationOpenedApp((remoteMessage) => - handleNotificationResponse({ - notification: mapRemoteMessage(remoteMessage), - actionIdentifier: DEFAULT_ACTION_IDENTIFIER, - }), - ) - - messaging().onMessage((remoteMessage) => - handleNotificationResponse({ - notification: mapRemoteMessage(remoteMessage), - actionIdentifier: ACTION_IDENTIFIER_NO_OPERATION, - }), - ) - - messaging().setBackgroundMessageHandler((remoteMessage) => - handleNotificationResponse({ - notification: mapRemoteMessage(remoteMessage), - actionIdentifier: ACTION_IDENTIFIER_NO_OPERATION, - }), - ) -} - -/** - * Handle initial notification when app is closed and opened from a notification - */ -export function handleInitialNotification() { - // FCMs - messaging() - .getInitialNotification() - .then((remoteMessage) => { - if (remoteMessage) { - void handleNotificationResponse({ - notification: mapRemoteMessage(remoteMessage), - actionIdentifier: DEFAULT_ACTION_IDENTIFIER, - }) - } - }) -} diff --git a/yarn.lock b/yarn.lock index 474666f89dde..911d3842134f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10753,8 +10753,8 @@ __metadata: linkType: hard "@expo/metro-config@npm:~0.18.6": - version: 0.18.7 - resolution: "@expo/metro-config@npm:0.18.7" + version: 0.18.11 + resolution: "@expo/metro-config@npm:0.18.11" dependencies: "@babel/core": ^7.20.0 "@babel/generator": ^7.20.5 @@ -10774,7 +10774,7 @@ __metadata: lightningcss: ~1.19.0 postcss: ~8.4.32 resolve-from: ^5.0.0 - checksum: f9212492ed5bb1d28bb506280055d7488f1d7d2013f65fdaaec8158de07cdd46c887d30ae206d65b89fee24bf1def20b28caf1563f54c3daabe02ad0d210ee3e + checksum: 4de79b97c6d818a487c6eaa83a55d3d9d1a1b28262507d74ad407fa22c2c32658d2cd2fa38babf82c32cf58239aff2c5d85e130609eaa34ed29a8e20a295cd7f languageName: node linkType: hard @@ -12544,6 +12544,7 @@ __metadata: expo: 51.0.25 expo-file-system: 17.0.1 expo-haptics: 13.0.1 + expo-linking: 6.3.1 expo-local-authentication: 14.0.1 expo-notifications: 0.28.9 intl: 1.2.5 @@ -24933,8 +24934,8 @@ __metadata: linkType: hard "babel-preset-expo@npm:~11.0.13": - version: 11.0.13 - resolution: "babel-preset-expo@npm:11.0.13" + version: 11.0.14 + resolution: "babel-preset-expo@npm:11.0.14" dependencies: "@babel/plugin-proposal-decorators": ^7.12.9 "@babel/plugin-transform-export-namespace-from": ^7.22.11 @@ -24946,7 +24947,7 @@ __metadata: babel-plugin-react-compiler: ^0.0.0-experimental-592953e-20240517 babel-plugin-react-native-web: ~0.19.10 react-refresh: ^0.14.2 - checksum: 6bfc721da903591bf94c73b711ead8ce5d28739fa6b5c893581c4c5f70f164aa6930982300066d412ce81e0c11e9e531e5c339751b05f002a37909e096f54b06 + checksum: b41c3fab6592fceb4ae020a0a79cb8e1d2e0354daca1d468e7db2c3033a17d654ac4627fb0b26f728809bc9810b7a1065dfd2a8a1f05fdbc83bacdc90e8e79dd languageName: node linkType: hard @@ -32265,13 +32266,13 @@ __metadata: linkType: hard "expo-font@npm:~12.0.9": - version: 12.0.9 - resolution: "expo-font@npm:12.0.9" + version: 12.0.10 + resolution: "expo-font@npm:12.0.10" dependencies: fontfaceobserver: ^2.1.0 peerDependencies: expo: "*" - checksum: adad225ed6002d5d527808b8f463bc59a1a1626fb2ff34918dcbd2172757977c056101f737ed9523f6d55e0aa88a64988002eb9b6d22f379d5956883f7451379 + checksum: c8fdc046158d4c2d71d81fcd9ba115bc0e142bc0d637ae9b5fea04cd816c62c051f63e44685530109106565d29feca2035ef6123c56cf9c951d0a2775a8cd9a7 languageName: node linkType: hard @@ -32293,6 +32294,16 @@ __metadata: languageName: node linkType: hard +"expo-linking@npm:6.3.1": + version: 6.3.1 + resolution: "expo-linking@npm:6.3.1" + dependencies: + expo-constants: ~16.0.0 + invariant: ^2.2.4 + checksum: 32e2dbcffc802fc6570a5a9cd7839c873f6cfc40730f1cf3cdabeb2782c30b54455d41c98708dbba2649941d5ff8cb591b85689f9c1a3b7a3fcb20011aae0cb5 + languageName: node + linkType: hard + "expo-local-authentication@npm:14.0.1": version: 14.0.1 resolution: "expo-local-authentication@npm:14.0.1"