// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import Emm from '@mattermost/react-native-emm'; import {Alert, DeviceEventEmitter, Linking, Platform} from 'react-native'; import {Notifications} from 'react-native-notifications'; import {appEntry, pushNotificationEntry, upgradeEntry} from '@actions/remote/entry'; import {Screens, DeepLink, Events, Launch, PushNotification} from '@constants'; import DatabaseManager from '@database/manager'; import {getActiveServerUrl, getServerCredentials, removeServerCredentials} from '@init/credentials'; import {onboadingViewedValue} from '@queries/app/global'; import {getThemeForCurrentTeam} from '@queries/servers/preference'; import {getCurrentUserId} from '@queries/servers/system'; import {queryMyTeams} from '@queries/servers/team'; import {goToScreen, resetToHome, resetToSelectServer, resetToTeams, resetToOnboarding} from '@screens/navigation'; import EphemeralStore from '@store/ephemeral_store'; import {logInfo} from '@utils/log'; import {convertToNotificationData} from '@utils/notification'; import {parseDeepLink} from '@utils/url'; import type {DeepLinkChannel, DeepLinkDM, DeepLinkGM, DeepLinkPermalink, DeepLinkWithData, LaunchProps} from '@typings/launch'; const initialNotificationTypes = [PushNotification.NOTIFICATION_TYPE.MESSAGE, PushNotification.NOTIFICATION_TYPE.SESSION]; export const initialLaunch = async () => { const deepLinkUrl = await Linking.getInitialURL(); if (deepLinkUrl) { launchAppFromDeepLink(deepLinkUrl); return; } const notification = await Notifications.getInitialNotification(); let tapped = Platform.select({android: true, ios: false})!; if (Platform.OS === 'ios' && notification) { // when a notification is received on iOS, getInitialNotification, will return the notification // as the app will initialized cause we are using background fetch, // that does not necessarily mean that the app was opened cause of the notification was tapped. // Here we are going to dettermine if the notification still exists in NotificationCenter to determine if // the app was opened because of a tap or cause of the background fetch init const delivered = await Notifications.ios.getDeliveredNotifications(); tapped = delivered.find((d) => (d as unknown as NotificationData).ack_id === notification?.payload.ack_id) == null; } if (initialNotificationTypes.includes(notification?.payload?.type) && tapped) { launchAppFromNotification(convertToNotificationData(notification!)); return; } launchApp({launchType: Launch.Normal}); }; const launchAppFromDeepLink = (deepLinkUrl: string) => { const props = getLaunchPropsFromDeepLink(deepLinkUrl); launchApp(props); }; const launchAppFromNotification = async (notification: NotificationWithData) => { const props = await getLaunchPropsFromNotification(notification); launchApp(props); }; const launchApp = async (props: LaunchProps, resetNavigation = true) => { const onboadingViewed = await onboadingViewedValue(); let serverUrl: string | undefined; switch (props?.launchType) { case Launch.DeepLink: if (props.extra?.type !== DeepLink.Invalid) { const extra = props.extra as DeepLinkWithData; serverUrl = extra.data?.serverUrl; } break; case Launch.Notification: { serverUrl = props.serverUrl; const extra = props.extra as NotificationWithData; const sessionExpiredNotification = Boolean(props.serverUrl && extra.payload?.type === PushNotification.NOTIFICATION_TYPE.SESSION); if (sessionExpiredNotification) { DeviceEventEmitter.emit(Events.SESSION_EXPIRED, serverUrl); return ''; } break; } default: serverUrl = await getActiveServerUrl(); break; } if (props.launchError && !serverUrl) { serverUrl = await getActiveServerUrl(); } if (serverUrl) { const credentials = await getServerCredentials(serverUrl); if (credentials) { const database = DatabaseManager.serverDatabases[serverUrl]?.database; let hasCurrentUser = false; if (database) { EphemeralStore.theme = await getThemeForCurrentTeam(database); const currentUserId = await getCurrentUserId(database); hasCurrentUser = Boolean(currentUserId); } if (!onboadingViewed) { return launchToOnboarding(props, resetNavigation, false, false, serverUrl); } let launchType = props.launchType; if (!hasCurrentUser) { // migrating from v1 if (launchType === Launch.Normal) { launchType = Launch.Upgrade; } const result = await upgradeEntry(serverUrl); if (result.error) { Alert.alert( 'Error Upgrading', `An error occurred while upgrading the app to the new version.\n\nDetails: ${result.error}\n\nThe app will now quit.`, [{ text: 'OK', onPress: async () => { await DatabaseManager.destroyServerDatabase(serverUrl!); await removeServerCredentials(serverUrl!); Emm.exitApp(); }, }], ); return ''; } } return launchToHome({...props, launchType, serverUrl}); } } if (onboadingViewed) { return launchToServer(props, resetNavigation); } return launchToOnboarding(props, resetNavigation); }; const launchToHome = async (props: LaunchProps) => { let openPushNotification = false; switch (props.launchType) { case Launch.DeepLink: // TODO: // deepLinkEntry({props.serverUrl, props.extra}); break; case Launch.Notification: { const extra = props.extra as NotificationWithData; openPushNotification = Boolean(props.serverUrl && !props.launchError && extra.userInteraction && extra.payload?.channel_id && !extra.payload?.userInfo?.local); if (openPushNotification) { pushNotificationEntry(props.serverUrl!, extra); } else { appEntry(props.serverUrl!); } break; } case Launch.Normal: appEntry(props.serverUrl!); break; } let nTeams = 0; if (props.serverUrl) { const database = DatabaseManager.serverDatabases[props.serverUrl]?.database; if (database) { nTeams = await queryMyTeams(database).fetchCount(); } } if (nTeams) { logInfo('Launch app in Home screen'); return resetToHome(props); } logInfo('Launch app in Select Teams screen'); return resetToTeams(); }; const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => { if (resetNavigation) { return resetToSelectServer(props); } // This is being called for Deeplinks, but needs to be revisited when // the implementation of deep links is complete const title = ''; return goToScreen(Screens.SERVER, title, {...props}); }; const launchToOnboarding = ( props: LaunchProps, resetNavigation = true, notActiveSession = true, whiteLabeledApp = false, goToLoginServerUrl = '', ) => { // here, if there is an active session, redirect to home // if there is a whitelabeled app, redirect to either SERVER or LOGIN but don't show the onboarding if (resetNavigation) { launchToServer(props, resetNavigation); } // goToLoginServerUrl will contain a value when there is a server already configured but not a active session return resetToOnboarding(props, goToLoginServerUrl); }; export const relaunchApp = (props: LaunchProps, resetNavigation = false) => { return launchApp(props, resetNavigation); }; export const getLaunchPropsFromDeepLink = (deepLinkUrl: string): LaunchProps => { const parsed = parseDeepLink(deepLinkUrl); const launchProps: LaunchProps = { launchType: Launch.DeepLink, }; switch (parsed.type) { case DeepLink.Invalid: launchProps.launchError = true; break; case DeepLink.Channel: { const parsedData = parsed.data as DeepLinkChannel; (launchProps.extra as DeepLinkWithData).data = parsedData; break; } case DeepLink.DirectMessage: { const parsedData = parsed.data as DeepLinkDM; (launchProps.extra as DeepLinkWithData).data = parsedData; break; } case DeepLink.GroupMessage: { const parsedData = parsed.data as DeepLinkGM; (launchProps.extra as DeepLinkWithData).data = parsedData; break; } case DeepLink.Permalink: { const parsedData = parsed.data as DeepLinkPermalink; (launchProps.extra as DeepLinkWithData).data = parsedData; break; } } return launchProps; }; export const getLaunchPropsFromNotification = async (notification: NotificationWithData): Promise => { const launchProps: LaunchProps = { launchType: Launch.Notification, }; const {payload} = notification; launchProps.extra = notification; if (payload?.server_url) { launchProps.serverUrl = payload.server_url; } else if (payload?.server_id) { const serverUrl = await DatabaseManager.getServerUrlFromIdentifier(payload.server_id); if (serverUrl) { launchProps.serverUrl = serverUrl; } else { launchProps.launchError = true; } } else { launchProps.launchError = true; } return launchProps; };