diff --git a/src/App.js b/src/App.js
index 483403170..ede511fc3 100644
--- a/src/App.js
+++ b/src/App.js
@@ -36,7 +36,8 @@ import { useNetService } from '/libs/services/NetService'
import { withSentry } from '/libs/monitoring/Sentry'
import { ThemeProvider } from '/app/theme/ThemeProvider'
import { useInitI18n } from '/locales/useInitI18n'
-import { SharingProvider } from '/app/view/sharing/SharingProvider'
+import { SharingProvider } from '/app/view/Sharing/SharingProvider'
+import { ErrorProvider } from '/app/view/Error/ErrorProvider'
// Polyfill needed for cozy-client connection
if (!global.btoa) {
@@ -57,7 +58,11 @@ const App = ({ setClient }) => {
const { initialRoute, isLoading } = useAppBootstrap(client)
- useGlobalAppState()
+ useGlobalAppState({
+ onNavigationRequest: route => {
+ RootNavigation.navigate(route)
+ }
+ })
useSecureBackgroundSplashScreen()
useCookieResyncOnResume()
useNotifications()
@@ -75,24 +80,28 @@ const Nav = ({ client, setClient }) => {
return (
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
)
}
@@ -159,9 +168,7 @@ const Wrapper = () => {
-
-
-
+
diff --git a/src/AppRouter.jsx b/src/AppRouter.jsx
index 1787821f4..0ba1c5de3 100644
--- a/src/AppRouter.jsx
+++ b/src/AppRouter.jsx
@@ -11,7 +11,7 @@ import { ManagerScreen } from '/app/view/Manager/ManagerScreen'
import { OnboardingScreen } from '/screens/login/OnboardingScreen'
import { WelcomeScreen } from '/screens/welcome/WelcomeScreen'
import { routes } from '/constants/routes'
-import { SharingScreen } from '/app/view/sharing/SharingScreen'
+import { SharingScreen } from '/app/view/Sharing/SharingScreen'
import { PasswordPrompt } from '/app/view/Secure/PasswordPrompt'
import { PinPrompt } from '/app/view/Secure/PinPrompt'
import { SetPasswordView } from '/app/view/Secure/SetPasswordView'
diff --git a/src/app/domain/authorization/services/SecurityService.ts b/src/app/domain/authorization/services/SecurityService.ts
index cb703fc5d..9db3004ec 100644
--- a/src/app/domain/authorization/services/SecurityService.ts
+++ b/src/app/domain/authorization/services/SecurityService.ts
@@ -29,6 +29,7 @@ import { navigateToApp } from '/libs/functions/openApp'
import { hideSplashScreen } from '/app/theme/SplashScreenService'
import { SecurityNavigationService } from '/app/domain/authorization/services/SecurityNavigationService'
import { getData, StorageKeys } from '/libs/localStore'
+import { SharingIntentStatus } from '/app/domain/sharing/models/SharingState'
// Can use mock functions in dev environment
const fns = getDevModeFunctions(
@@ -79,15 +80,18 @@ export const determineSecurityFlow = async (
href: string
slug: string
},
- isCallerHandlingSplashscreen?: boolean
+ isCallerHandlingSplashscreen?: boolean,
+ sharingIntentStatus?: SharingIntentStatus
): Promise => {
+ const openedWithSharing =
+ sharingIntentStatus === SharingIntentStatus.OpenedViaSharing
SecurityNavigationService.startListening()
const callbackNav = async (): Promise => {
try {
if (navigationObject) {
await navigateToApp(navigationObject)
- } else navigate(routes.home)
+ } else navigate(openedWithSharing ? routes.sharing : routes.home)
} catch (error) {
devlog('🔏', 'Error navigating to app, defaulting to home', error)
navigate(routes.home)
diff --git a/src/app/domain/sharing/models/SharingCozyApp.ts b/src/app/domain/sharing/models/SharingCozyApp.ts
new file mode 100644
index 000000000..ae995fabb
--- /dev/null
+++ b/src/app/domain/sharing/models/SharingCozyApp.ts
@@ -0,0 +1,21 @@
+export interface SharingCozyApp {
+ _id: string
+ _type: string
+ accept_documents_from_flagship?: {
+ accepted_mime_types: string[]
+ max_number_of_files: number
+ max_size_per_file_in_MB: number
+ route_to_upload: string
+ }
+ accept_from_flagship?: boolean
+ attributes: {
+ accept_documents_from_flagship?: {
+ accepted_mime_types: string[]
+ max_number_of_files: number
+ max_size_per_file_in_MB: number
+ route_to_upload: string
+ }
+ accept_from_flagship?: boolean
+ }
+ slug: string
+}
diff --git a/src/app/domain/sharing/models/SharingState.ts b/src/app/domain/sharing/models/SharingState.ts
index 9c83755c3..65f0f662c 100644
--- a/src/app/domain/sharing/models/SharingState.ts
+++ b/src/app/domain/sharing/models/SharingState.ts
@@ -8,14 +8,30 @@ export enum SharingIntentStatus {
export enum SharingActionType {
SetIntentStatus = 'SET_INTENT_STATUS',
- SetFilesToUpload = 'SET_FILES_TO_UPLOAD'
+ SetFilesToUpload = 'SET_FILES_TO_UPLOAD',
+ SetRouteToUpload = 'SET_ROUTE_TO_UPLOAD',
+ SetFlowErrored = 'SET_FLOW_ERRORED',
+ SetRecoveryState = 'SET_RECOVERY_STATE'
}
export interface SharingState {
sharingIntentStatus: SharingIntentStatus
filesToUpload: ReceivedFile[]
+ routeToUpload?: { href: string; slug: string }
+ errored: boolean
}
export type SharingAction =
| { type: SharingActionType.SetIntentStatus; payload: SharingIntentStatus }
| { type: SharingActionType.SetFilesToUpload; payload: ReceivedFile[] }
+ | {
+ type: SharingActionType.SetRouteToUpload
+ payload: { href: string; slug: string }
+ }
+ | { type: SharingActionType.SetFlowErrored; payload: boolean }
+ | { type: SharingActionType.SetRecoveryState }
+
+export interface ServiceResponse {
+ result?: T
+ error?: string
+}
diff --git a/src/app/domain/sharing/services/SharingNetwork.spec.ts b/src/app/domain/sharing/services/SharingNetwork.spec.ts
new file mode 100644
index 000000000..aeff9a2f2
--- /dev/null
+++ b/src/app/domain/sharing/services/SharingNetwork.spec.ts
@@ -0,0 +1,59 @@
+import CozyClient from 'cozy-client'
+
+import { SharingCozyApp } from '/app/domain/sharing/models/SharingCozyApp'
+import { getRouteToUpload } from '/app/domain/sharing/services/SharingNetwork'
+
+describe('getRouteToUpload', () => {
+ const mockCozyClient = new CozyClient({
+ uri: 'http://cozy.local',
+ capabilities: { flat_subdomains: true }
+ })
+
+ it('returns empty object if no client is provided', () => {
+ const result = getRouteToUpload()
+ expect(result).toEqual({})
+ })
+
+ it('returns empty object if cozyApps is not an array or empty', () => {
+ // @ts-expect-error Testing invalid input
+ const result = getRouteToUpload(null, mockCozyClient)
+ expect(result).toEqual({})
+ })
+
+ it('returns empty object if no matching app is found', () => {
+ const cozyApps = [{ slug: 'wrong-app' } as SharingCozyApp]
+
+ const result = getRouteToUpload(cozyApps, mockCozyClient)
+ expect(result).toEqual({})
+ })
+
+ it('returns the correct href and slug', () => {
+ const cozyApps = [
+ {
+ slug: 'drive',
+ accept_documents_from_flagship: {
+ route_to_upload: '/upload-route'
+ },
+ attributes: {
+ accept_documents_from_flagship: {
+ route_to_upload: '/upload-route'
+ }
+ }
+ } as SharingCozyApp
+ ]
+ const result = getRouteToUpload(cozyApps, mockCozyClient, 'drive')
+ expect(result).toEqual({
+ result: {
+ href: 'http://cozy-drive.local/#/upload-route',
+ slug: 'drive'
+ }
+ })
+ })
+
+ it('handles an error gracefully', () => {
+ const brokenClient = new CozyClient({ uri: undefined })
+ const cozyApps = [{ slug: 'drive' } as SharingCozyApp]
+ const result = getRouteToUpload(cozyApps, brokenClient)
+ expect(result).toEqual({ error: 'Error determining route to upload.' })
+ })
+})
diff --git a/src/app/domain/sharing/services/SharingNetwork.ts b/src/app/domain/sharing/services/SharingNetwork.ts
new file mode 100644
index 000000000..59cfccfae
--- /dev/null
+++ b/src/app/domain/sharing/services/SharingNetwork.ts
@@ -0,0 +1,48 @@
+import CozyClient, { generateWebLink, Q } from 'cozy-client'
+
+import { sharingLogger } from '/app/domain/sharing'
+import { SharingCozyApp } from '/app/domain/sharing/models/SharingCozyApp'
+import { ServiceResponse } from '/app/domain/sharing/models/SharingState'
+
+export const fetchSharingCozyApps = {
+ definition: Q('io.cozy.apps').where({
+ 'accept_documents_from_flagship.route_to_upload': { $exists: true },
+ accept_from_flagship: true
+ }),
+ options: {
+ as: 'io.cozy.apps/fetchSharingCozyApps'
+ }
+}
+
+export const getRouteToUpload = (
+ cozyApps?: SharingCozyApp[],
+ client?: CozyClient | null,
+ appName = 'drive'
+): ServiceResponse<{ href: string; slug: string }> => {
+ try {
+ if (!client || !Array.isArray(cozyApps) || cozyApps.length === 0) return {}
+
+ const cozyApp = cozyApps.find(cozyApp => cozyApp.slug === appName)
+ if (!cozyApp) return {}
+ const hash =
+ cozyApp.accept_documents_from_flagship?.route_to_upload ??
+ cozyApp.attributes.accept_documents_from_flagship?.route_to_upload
+ const slug = cozyApp.slug
+ if (!hash || !slug) return {}
+
+ const href = generateWebLink({
+ cozyUrl: client.getStackClient().uri,
+ pathname: '',
+ slug,
+ subDomainType: client.capabilities.flat_subdomains ? 'flat' : 'nested',
+ hash: hash.replace(/^\/?#?\//, ''),
+ searchParams: []
+ })
+
+ sharingLogger.info('routeToUpload is', { href, slug })
+ return { result: { href, slug: cozyApp.slug } }
+ } catch (error) {
+ sharingLogger.error('Error when getting routeToUpload', error)
+ return { error: 'Error determining route to upload.' }
+ }
+}
diff --git a/src/app/domain/sharing/services/SharingStatus.ts b/src/app/domain/sharing/services/SharingStatus.ts
index 28d00053a..f9f3766ee 100644
--- a/src/app/domain/sharing/services/SharingStatus.ts
+++ b/src/app/domain/sharing/services/SharingStatus.ts
@@ -33,9 +33,7 @@ export const handleSharing = (
: SharingIntentStatus.NotOpenedViaSharing
sharingLogger.info(
- `${
- isAndroid ? 'Android' : 'iOS'
- } App was opened or resumed via sharing, setting status to ${newStatus}`
+ `App was opened or resumed via sharing, setting status to ${newStatus}`
)
setStatus(newStatus)
diff --git a/src/app/view/Error/ErrorProvider.tsx b/src/app/view/Error/ErrorProvider.tsx
new file mode 100644
index 000000000..2473de12e
--- /dev/null
+++ b/src/app/view/Error/ErrorProvider.tsx
@@ -0,0 +1,62 @@
+import React, { ReactNode, useCallback, useEffect } from 'react'
+
+import { ErrorToaster } from '/app/view/Error/ErrorToaster'
+
+interface ErrorState {
+ message: string | null
+}
+
+export const ErrorStateContext = React.createContext({
+ message: null
+})
+export const ErrorDispatchContext = React.createContext<
+ React.Dispatch>
+>(() => {
+ // eslint-disable-next-line no-console
+ console.warn('ErrorDispatchContext used outside of ErrorProvider.')
+})
+
+export const ErrorProvider = ({
+ children
+}: {
+ children: ReactNode
+}): JSX.Element => {
+ const [error, setError] = React.useState(null)
+
+ useEffect(() => {
+ if (error) {
+ const timer = setTimeout(() => setError(null), 3000)
+ return () => clearTimeout(timer)
+ }
+ }, [error])
+
+ return (
+
+
+ {error ? : null}
+ {children}
+
+
+ )
+}
+
+interface ErrorHook {
+ error: ErrorState
+ setError: React.Dispatch>
+ handleError: (errorMessage: string, callback?: () => void) => void
+}
+
+export const useError = (): ErrorHook => {
+ const error = React.useContext(ErrorStateContext)
+ const setError = React.useContext(ErrorDispatchContext)
+
+ const handleError = useCallback(
+ (errorMessage: string, callback?: () => void): void => {
+ setError(errorMessage)
+ callback?.()
+ },
+ [setError]
+ )
+
+ return { error, setError, handleError }
+}
diff --git a/src/app/view/Error/ErrorToaster.tsx b/src/app/view/Error/ErrorToaster.tsx
new file mode 100644
index 000000000..a5a5e36db
--- /dev/null
+++ b/src/app/view/Error/ErrorToaster.tsx
@@ -0,0 +1,51 @@
+import React from 'react'
+import { Modal, Text, View, StyleSheet } from 'react-native'
+
+import { useError } from '/app/view/Error/ErrorProvider'
+
+export const ErrorToaster = (): JSX.Element => {
+ const { error, setError } = useError()
+
+ return (
+ setError(null)}
+ >
+
+
+ {error.message}
+
+
+
+ )
+}
+
+const styles = StyleSheet.create({
+ centeredView: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ marginTop: 22
+ },
+ modalView: {
+ margin: 20,
+ backgroundColor: 'white',
+ borderRadius: 20,
+ padding: 35,
+ alignItems: 'center',
+ shadowColor: '#000',
+ shadowOffset: {
+ width: 0,
+ height: 2
+ },
+ shadowOpacity: 0.25,
+ shadowRadius: 4,
+ elevation: 5
+ },
+ modalText: {
+ marginBottom: 15,
+ textAlign: 'center'
+ }
+})
diff --git a/src/app/view/sharing/SharingProvider.spec.tsx b/src/app/view/Sharing/SharingProvider.spec.tsx
similarity index 73%
rename from src/app/view/sharing/SharingProvider.spec.tsx
rename to src/app/view/Sharing/SharingProvider.spec.tsx
index 65cf79309..6b629103a 100644
--- a/src/app/view/sharing/SharingProvider.spec.tsx
+++ b/src/app/view/Sharing/SharingProvider.spec.tsx
@@ -1,18 +1,37 @@
-import React from 'react'
import { render } from '@testing-library/react-native'
+import React from 'react'
import { Text } from 'react-native'
-import {
- SharingProvider,
- useSharingState
-} from '/app/view/sharing/SharingProvider'
+import { useQuery } from 'cozy-client'
+
+import { SharingProvider } from '/app/view/Sharing/SharingProvider'
import { handleReceivedFiles } from '/app/domain/sharing/services/SharingData'
import { handleSharing } from '/app/domain/sharing/services/SharingStatus'
import { SharingIntentStatus } from '/app/domain/sharing/models/SharingState'
import { ReceivedFile } from '/app/domain/sharing/models/ReceivedFile'
+import { useSharingState } from '/app/view/Sharing/SharingState'
jest.mock('/app/domain/sharing/services/SharingData')
jest.mock('/app/domain/sharing/services/SharingStatus')
+jest.mock('/app/domain/sharing/services/SharingNetwork')
+jest.mock('/app/view/Error/ErrorProvider', () => ({
+ useError: jest.fn().mockReturnValue({
+ handleError: jest.fn()
+ })
+}))
+jest.mock('cozy-client', () => ({
+ useClient: jest.fn(),
+ useQuery: jest.fn(),
+ Q: jest.fn().mockReturnValue({
+ where: jest.fn().mockReturnValue({
+ all: jest.fn().mockReturnValue([])
+ })
+ })
+}))
+jest.mock('@react-navigation/native', () => ({
+ useNavigation: jest.fn(),
+ useNavigationState: jest.fn()
+}))
describe('SharingProvider', () => {
beforeEach(() => {
@@ -24,7 +43,7 @@ describe('SharingProvider', () => {
it('calls services and updates state correctly on mount', () => {
const mockReceivedFilesCallback = jest.fn()
const mockSharingCallback = jest.fn()
-
+ ;(useQuery as jest.Mock).mockReturnValue({ data: [] })
;(handleReceivedFiles as jest.Mock).mockImplementation(callback => {
mockReceivedFilesCallback()
const mockCallback = callback as (files: ReceivedFile[]) => void
@@ -105,4 +124,23 @@ describe('SharingProvider', () => {
getByText(SharingIntentStatus.NotOpenedViaSharing.toString())
})
+
+ it('calls cleanup functions on unmount', () => {
+ const cleanupReceivedFiles = jest.fn()
+ const cleanupSharingIntent = jest.fn()
+
+ ;(handleReceivedFiles as jest.Mock).mockReturnValue(cleanupReceivedFiles)
+ ;(handleSharing as jest.Mock).mockReturnValue(cleanupSharingIntent)
+
+ const { unmount } = render(
+
+ Test
+
+ )
+
+ unmount()
+
+ expect(cleanupReceivedFiles).toHaveBeenCalledTimes(1)
+ expect(cleanupSharingIntent).toHaveBeenCalledTimes(1)
+ })
})
diff --git a/src/app/view/Sharing/SharingProvider.tsx b/src/app/view/Sharing/SharingProvider.tsx
new file mode 100644
index 000000000..e6ac44071
--- /dev/null
+++ b/src/app/view/Sharing/SharingProvider.tsx
@@ -0,0 +1,117 @@
+import { useNavigation, useNavigationState } from '@react-navigation/native'
+import React, { useReducer, useEffect, useCallback } from 'react'
+
+import { useClient, useQuery } from 'cozy-client'
+
+import { SharingCozyApp } from '/app/domain/sharing/models/SharingCozyApp'
+import { handleReceivedFiles } from '/app/domain/sharing/services/SharingData'
+import { handleSharing } from '/app/domain/sharing/services/SharingStatus'
+import { useError } from '/app/view/Error/ErrorProvider'
+import { useI18n } from '/locales/i18n'
+import {
+ SharingIntentStatus,
+ SharingActionType
+} from '/app/domain/sharing/models/SharingState'
+import {
+ fetchSharingCozyApps,
+ getRouteToUpload
+} from '/app/domain/sharing/services/SharingNetwork'
+import {
+ initialState,
+ SharingDispatchContext,
+ sharingReducer,
+ SharingStateContext
+} from '/app/view/Sharing/SharingState'
+import { routes } from '/constants/routes'
+
+export const SharingProvider = ({
+ children
+}: React.PropsWithChildren): JSX.Element => {
+ const client = useClient()
+ const [state, dispatch] = useReducer(sharingReducer, initialState)
+ const { t } = useI18n()
+ const { handleError } = useError()
+ const navigationState = useNavigationState(state => state)
+ const navigation = useNavigation()
+ const { data } = useQuery(
+ fetchSharingCozyApps.definition,
+ fetchSharingCozyApps.options
+ ) as { data?: SharingCozyApp[] | [] }
+
+ const isProcessed = useCallback(
+ (): boolean => state.filesToUpload.length > 1 || state.errored,
+ [state.filesToUpload, state.errored]
+ )
+ const hasData = useCallback(
+ (): boolean =>
+ Boolean(
+ state.filesToUpload.length > 0 && client && data && data.length > 0
+ ),
+ [client, data, state.filesToUpload]
+ )
+
+ // This effect is triggered at mount and unmount of the provider,
+ // its role is to listen native events and update the state accordingly
+ useEffect(() => {
+ // As soon as we can detect that the app was opened with or without files,
+ // we can update the state accordingly so the view can react to it
+ const cleanupSharingIntent = handleSharing(
+ (status: SharingIntentStatus) => {
+ dispatch({ type: SharingActionType.SetIntentStatus, payload: status })
+ }
+ )
+
+ // Pass a callback to the low level function that handles the received files
+ // We will have access to their paths in the provider state afterwards
+ const cleanupReceivedFiles = handleReceivedFiles(files => {
+ dispatch({ type: SharingActionType.SetFilesToUpload, payload: files })
+ })
+
+ return () => {
+ cleanupReceivedFiles()
+ cleanupSharingIntent()
+ }
+ }, [])
+
+ // Fetches the route of the cozy-app that will handle the sharing intent
+ useEffect(() => {
+ if (isProcessed() || !hasData()) return
+ const { result, error } = getRouteToUpload(data, client)
+
+ if (error) {
+ dispatch({ type: SharingActionType.SetFlowErrored, payload: true })
+ } else if (result !== undefined) {
+ dispatch({ type: SharingActionType.SetRouteToUpload, payload: result })
+ }
+ }, [client, data, handleError, hasData, isProcessed])
+
+ // If an error is detected, we handle that by abandoning the flow.
+ // The user will be redirected to the home screen and the sharing mode is ended until next file sharing.
+ useEffect(() => {
+ if (state.errored) {
+ dispatch({ type: SharingActionType.SetRecoveryState })
+ if (navigationState.routes[navigationState.index].name !== routes.lock) {
+ handleError(t('errors.unknown_error'), () => {
+ navigation.navigate(routes.home as never)
+ })
+ }
+ }
+ }, [
+ handleError,
+ navigation,
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ navigationState?.index,
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ navigationState?.routes,
+ state.errored,
+ t
+ ])
+
+ return (
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/app/view/sharing/SharingScreen.tsx b/src/app/view/Sharing/SharingScreen.tsx
similarity index 91%
rename from src/app/view/sharing/SharingScreen.tsx
rename to src/app/view/Sharing/SharingScreen.tsx
index fdb4fc2fe..b66d4e0a5 100644
--- a/src/app/view/sharing/SharingScreen.tsx
+++ b/src/app/view/Sharing/SharingScreen.tsx
@@ -3,7 +3,7 @@ import React from 'react'
import { Container } from '/ui/Container'
import { Grid } from '/ui/Grid'
import { Typography } from '/ui/Typography'
-import { useSharingState } from '/app/view/sharing/SharingProvider'
+import { useSharingState } from '/app/view/Sharing/SharingState'
export const SharingScreen = (): JSX.Element => {
const { filesToUpload } = useSharingState()
diff --git a/src/app/view/Sharing/SharingState.ts b/src/app/view/Sharing/SharingState.ts
new file mode 100644
index 000000000..70bd4c78c
--- /dev/null
+++ b/src/app/view/Sharing/SharingState.ts
@@ -0,0 +1,71 @@
+import { createContext, Dispatch, useContext } from 'react'
+
+import { sharingLogger } from '/app/domain/sharing'
+import {
+ SharingState,
+ SharingAction,
+ SharingActionType,
+ SharingIntentStatus
+} from '/app/domain/sharing/models/SharingState'
+
+export const SharingStateContext = createContext(
+ undefined
+)
+export const SharingDispatchContext = createContext<
+ Dispatch | undefined
+>(undefined)
+
+export const sharingReducer = (
+ state: SharingState,
+ action: SharingAction
+): SharingState => {
+ let nextState = state
+
+ switch (action.type) {
+ case SharingActionType.SetIntentStatus:
+ if (state.sharingIntentStatus === action.payload) return state
+ nextState = { ...state, sharingIntentStatus: action.payload }
+ break
+ case SharingActionType.SetFilesToUpload:
+ if (state.filesToUpload === action.payload) return state
+ nextState = { ...state, filesToUpload: action.payload }
+ break
+ case SharingActionType.SetRouteToUpload:
+ if (state.routeToUpload?.slug === action.payload.slug) return state
+ nextState = { ...state, routeToUpload: action.payload }
+ break
+ case SharingActionType.SetFlowErrored: {
+ nextState = { ...state, errored: action.payload }
+ break
+ }
+ case SharingActionType.SetRecoveryState:
+ nextState = {
+ ...initialState,
+ sharingIntentStatus: SharingIntentStatus.NotOpenedViaSharing
+ }
+ break
+ default:
+ break
+ }
+
+ sharingLogger.info(`sharingReducer handled action "${action.type}"`)
+ sharingLogger.info('sharingReducer prevState', state)
+ sharingLogger.info('sharingReducer nextState', nextState)
+
+ return nextState
+}
+
+export const initialState: SharingState = {
+ sharingIntentStatus: SharingIntentStatus.Undetermined,
+ filesToUpload: [],
+ routeToUpload: undefined,
+ errored: false
+}
+
+export const useSharingState = (): SharingState => {
+ const context = useContext(SharingStateContext)
+ if (context === undefined) {
+ throw new Error('useSharingState must be used within a SharingProvider')
+ }
+ return context
+}
diff --git a/src/app/view/sharing/SharingProvider.tsx b/src/app/view/sharing/SharingProvider.tsx
deleted file mode 100644
index d1980311c..000000000
--- a/src/app/view/sharing/SharingProvider.tsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import React, { useReducer, ReactNode, Dispatch, useEffect } from 'react'
-
-import {
- SharingState,
- SharingAction,
- SharingIntentStatus,
- SharingActionType
-} from '/app/domain/sharing/models/SharingState'
-import { handleReceivedFiles } from '/app/domain/sharing/services/SharingData'
-import { handleSharing } from '/app/domain/sharing/services/SharingStatus'
-
-const assertNever = (action: never): never => {
- throw new Error('Unexpected object', action)
-}
-
-export const sharingReducer = (
- state: SharingState,
- action: SharingAction
-): SharingState => {
- switch (action.type) {
- case SharingActionType.SetIntentStatus:
- return { ...state, sharingIntentStatus: action.payload }
- case SharingActionType.SetFilesToUpload:
- return { ...state, filesToUpload: action.payload }
- default:
- return assertNever(action)
- }
-}
-
-const SharingStateContext = React.createContext(
- undefined
-)
-const SharingDispatchContext = React.createContext<
- Dispatch | undefined
->(undefined)
-
-interface SharingProviderProps {
- children: ReactNode
-}
-
-export const SharingProvider = ({
- children
-}: SharingProviderProps): JSX.Element => {
- const [state, dispatch] = useReducer(sharingReducer, {
- sharingIntentStatus: SharingIntentStatus.Undetermined,
- filesToUpload: []
- })
-
- useEffect(() => {
- const cleanupSharingIntent = handleSharing(
- (status: SharingIntentStatus) => {
- dispatch({ type: SharingActionType.SetIntentStatus, payload: status })
- }
- )
-
- const cleanupReceivedFiles = handleReceivedFiles(files => {
- dispatch({ type: SharingActionType.SetFilesToUpload, payload: files })
- })
-
- return () => {
- cleanupReceivedFiles()
- cleanupSharingIntent()
- }
- }, [dispatch])
-
- return (
-
-
- {children}
-
-
- )
-}
-
-export const useSharingState = (): SharingState => {
- const context = React.useContext(SharingStateContext)
- if (context === undefined) {
- throw new Error('useSharingState must be used within a SharingProvider')
- }
- return context
-}
-
-export const useSharingDispatch = (): Dispatch => {
- const context = React.useContext(SharingDispatchContext)
- if (context === undefined) {
- throw new Error('useSharingDispatch must be used within a SharingProvider')
- }
- return context
-}
diff --git a/src/hooks/useGlobalAppState.ts b/src/hooks/useGlobalAppState.ts
index a0749d8b1..c109e5aca 100644
--- a/src/hooks/useGlobalAppState.ts
+++ b/src/hooks/useGlobalAppState.ts
@@ -1,5 +1,4 @@
-import { useNavigationState } from '@react-navigation/native'
-import { useEffect } from 'react'
+import { useEffect, useRef } from 'react'
import { AppState, AppStateStatus, NativeEventSubscription } from 'react-native'
import CozyClient, { useClient } from 'cozy-client'
@@ -14,9 +13,8 @@ import {
} from '/app/domain/authorization/services/SecurityService'
import { devlog } from '/core/tools/env'
import { synchronizeDevice } from '/app/domain/authentication/services/SynchronizeService'
-import { navigate } from '/libs/RootNavigation'
import { routes } from '/constants/routes'
-import { useSharingState } from '/app/view/sharing/SharingProvider'
+import { useSharingState } from '/app/view/Sharing/SharingState'
import { SharingIntentStatus } from '/app/domain/sharing/models/SharingState'
const log = Minilog('useGlobalAppState')
@@ -35,13 +33,14 @@ const handleSleep = (): void => {
const handleWakeUp = async (
client: CozyClient,
- sharingIntentStatus: SharingIntentStatus
+ sharingIntentStatus: SharingIntentStatus,
+ onNavigationRequest: (route: string) => void
): Promise => {
await handleSecurityFlowWakeUp(client)
if (sharingIntentStatus === SharingIntentStatus.OpenedViaSharing) {
log.info('useGlobalAppState: handleWakeUp, sharing mode')
- navigate(routes.sharing)
+ onNavigationRequest(routes.sharing)
}
}
@@ -54,13 +53,14 @@ const isGoingToWakeUp = (nextAppState: AppStateStatus): boolean =>
const onStateChange = (
nextAppState: AppStateStatus,
client: CozyClient,
- sharingIntentStatus: SharingIntentStatus
+ sharingIntentStatus: SharingIntentStatus,
+ onNavigationRequest: (route: string) => void
): void => {
if (isGoingToSleep(nextAppState)) handleSleep()
if (isGoingToWakeUp(nextAppState)) {
Promise.all([
- handleWakeUp(client, sharingIntentStatus),
+ handleWakeUp(client, sharingIntentStatus, onNavigationRequest),
synchronizeDevice(client)
]).catch(reason => log.error('Failed when waking up', reason))
}
@@ -75,45 +75,31 @@ const onStateChange = (
* Do NOT use it anywhere else than in the component,
* for it could create unintended side effects.
*/
-export const useGlobalAppState = (): void => {
- const client = useClient()
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
- const routeIndex = useNavigationState(state => state?.index)
- const { sharingIntentStatus } = useSharingState()
-
- // On app start
- useEffect(() => {
- const appStart = async (): Promise => {
- if (await getIsSecurityFlowPassed()) {
- log.info('useGlobalAppState: app start, security flow passed')
- if (sharingIntentStatus === SharingIntentStatus.OpenedViaSharing) {
- log.info('useGlobalAppState: app start, sharing mode')
- navigate(routes.sharing)
- } else {
- log.info('useGlobalAppState: app start, not sharing mode')
- }
- } else {
- log.info('useGlobalAppState: app start, security flow not passed')
- }
- }
+interface GlobalAppStateProps {
+ onNavigationRequest: (route: string) => void
+}
- log.info('useGlobalAppState: app start')
- void appStart()
- }, [routeIndex, sharingIntentStatus])
+export const useGlobalAppState = ({
+ onNavigationRequest
+}: GlobalAppStateProps): void => {
+ // Ref to track if the logic has already been executed
+ const hasExecuted = useRef(false)
+ const client = useClient()
+ const { sharingIntentStatus, errored } = useSharingState()
useEffect(() => {
let subscription: NativeEventSubscription | undefined
// If there's no client, we don't need to listen to app state changes
// because we can't lock the app anyway
- if (client && !subscription) {
+ if (!hasExecuted.current && client && !subscription) {
devlog(
'useGlobalAppState: subscribing to AppState changes, synchronizing device'
)
subscription = AppState.addEventListener('change', e =>
- onStateChange(e, client, sharingIntentStatus)
+ onStateChange(e, client, sharingIntentStatus, onNavigationRequest)
)
}
@@ -121,5 +107,31 @@ export const useGlobalAppState = (): void => {
appState = AppState.currentState
subscription?.remove()
}
- }, [client, sharingIntentStatus])
+ }, [client, onNavigationRequest, sharingIntentStatus])
+
+ // On app start
+ useEffect(() => {
+ const appStart = async (): Promise => {
+ if (await getIsSecurityFlowPassed()) {
+ log.info('useGlobalAppState: app start, security flow passed')
+
+ if (sharingIntentStatus === SharingIntentStatus.OpenedViaSharing) {
+ log.info('useGlobalAppState: app start, sharing mode')
+ !errored && onNavigationRequest(routes.sharing)
+ } else {
+ log.info('useGlobalAppState: app start, not sharing mode')
+ }
+ } else {
+ log.info('useGlobalAppState: app start, security flow not passed')
+ }
+ }
+
+ if (!hasExecuted.current) {
+ log.info('useGlobalAppState: app start')
+ void appStart()
+
+ // Mark the logic as executed
+ hasExecuted.current = true
+ }
+ }, [errored, onNavigationRequest, sharingIntentStatus])
}
diff --git a/src/screens/home/components/HomeView.js b/src/screens/home/components/HomeView.js
index 311defdd0..965cf3d0b 100644
--- a/src/screens/home/components/HomeView.js
+++ b/src/screens/home/components/HomeView.js
@@ -22,6 +22,7 @@ import { useHomeStateContext } from '/screens/home/HomeStateProvider'
import { launcherEvent } from '/libs/ReactNativeLauncher'
import { determineSecurityFlow } from '/app/domain/authorization/services/SecurityService'
import { devlog } from '/core/tools/env'
+import { useSharingState } from '/app/view/Sharing/SharingState'
const log = Minilog('🏠 HomeView')
@@ -59,6 +60,7 @@ const HomeView = ({ route, navigation, setLauncherContext, setBarStyle }) => {
const session = useSession()
const didBlurOnce = useRef(false)
const [webviewRef, setParentRef] = useState()
+ const { sharingIntentStatus } = useSharingState()
const mainAppFallbackURLInitialParam = useInitialParam(
'mainAppFallbackURL',
route,
@@ -238,7 +240,12 @@ const HomeView = ({ route, navigation, setLauncherContext, setBarStyle }) => {
`HomeView: setting hasRenderedOnce.current set to "true" and calling determineSecurityFlowHook()`
)
hasRenderedOnce.current = true
- await determineSecurityFlow(client, navigationObject, true)
+ await determineSecurityFlow(
+ client,
+ navigationObject,
+ true,
+ sharingIntentStatus
+ )
}
}
@@ -249,7 +256,8 @@ const HomeView = ({ route, navigation, setLauncherContext, setBarStyle }) => {
navigation,
shouldWaitCozyApp,
setShouldWaitCozyApp,
- uri
+ uri,
+ sharingIntentStatus
])
const handleTrackWebviewInnerUri = webviewInneruri => {