Skip to content

Commit

Permalink
feat(native-app): Implement universal links (#15961)
Browse files Browse the repository at this point in the history
- Only runs when the user is logged in and has passed the pin screen.
- Improves notification handling to support passkey browser.

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
eirikurn and kodiakhq[bot] authored Sep 16, 2024
1 parent a88bbcf commit 46a5b69
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 156 deletions.
12 changes: 12 additions & 0 deletions apps/native/app/android/app/src/prod/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="is.island.app" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="island.is" />
<data android:pathPrefix="/minarsidur/postholf" />
<data android:pathPrefix="/minarsidur/skirteini" />
<data android:pathPrefix="/minarsidur/eignir/fasteignir" />
<data android:pathPrefix="/minarsidur/eignir/okutaeki/min-okutaeki" />
<data android:pathPrefix="/minarsidur/loftbru" />
</intent-filter>
</activity>
<activity
android:name="net.openid.appauth.RedirectUriReceiverActivity">
Expand Down
1 change: 1 addition & 0 deletions apps/native/app/ios/IslandApp/IslandApp.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<key>com.apple.developer.associated-domains</key>
<array>
<string>webcredentials:island.is</string>
<string>applinks:island.is</string>
</array>
<key>keychain-access-groups</key>
<array>
Expand Down
1 change: 1 addition & 0 deletions apps/native/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
73 changes: 73 additions & 0 deletions apps/native/app/src/hooks/use-deep-link-handling.ts
Original file line number Diff line number Diff line change
@@ -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<FirebaseMessagingTypes.RemoteMessage | null>(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<string | null>(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])
}
4 changes: 0 additions & 4 deletions apps/native/app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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()

Expand Down
11 changes: 7 additions & 4 deletions apps/native/app/src/lib/deep-linking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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',
Expand Down
37 changes: 18 additions & 19 deletions apps/native/app/src/screens/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -150,6 +150,8 @@ export const MainHomeScreen: NavigationFunctionComponent = ({
({ widgetsInitialised }) => widgetsInitialised,
)

useDeepLinkHandling()

const applicationsRes = useListApplicationsQuery({
skip: !applicationsWidgetEnabled,
})
Expand Down Expand Up @@ -258,9 +260,6 @@ export const MainHomeScreen: NavigationFunctionComponent = ({
checkUnseen()
// Get user locale from server
getAndSetLocale()

// Handle initial notification
handleInitialNotification()
}, [])

const refetch = useCallback(async () => {
Expand Down
24 changes: 15 additions & 9 deletions apps/native/app/src/screens/notifications/notifications.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -85,6 +86,7 @@ export const NotificationsScreen: NavigationFunctionComponent = ({
componentId,
}) => {
useNavigationOptions(componentId)
const { openBrowser } = useBrowser()
const intl = useIntl()
const theme = useTheme()
const client = useApolloClient()
Expand Down Expand Up @@ -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 (
Expand Down
16 changes: 0 additions & 16 deletions apps/native/app/src/utils/lifecycle/setup-event-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,6 @@ let backgroundAppLockTimeout: ReturnType<typeof setTimeout>
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('#')
Expand Down Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 46a5b69

Please sign in to comment.