Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(native-app): Implement universal links #15961

Merged
merged 2 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" />
eirikurn marked this conversation as resolved.
Show resolved Hide resolved
</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(
eirikurn marked this conversation as resolved.
Show resolved Hide resolved
(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
Loading