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"