diff --git a/app/android/app/gradle.lockfile b/app/android/app/gradle.lockfile index 34f1c596a..37d5d002e 100644 --- a/app/android/app/gradle.lockfile +++ b/app/android/app/gradle.lockfile @@ -303,6 +303,7 @@ org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4=_internal-unified-test-platf org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1=debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.jetbrains:annotations:13.0=_internal-unified-test-platform-android-device-provider-gradle,_internal-unified-test-platform-android-test-plugin-host-additional-test-output,_internal-unified-test-platform-android-test-plugin-host-apk-installer,_internal-unified-test-platform-android-test-plugin-host-coverage,_internal-unified-test-platform-android-test-plugin-host-device-info,_internal-unified-test-platform-android-test-plugin-host-emulator-control,_internal-unified-test-platform-android-test-plugin-host-logcat,_internal-unified-test-platform-android-test-plugin-host-retention,_internal-unified-test-platform-android-test-plugin-result-listener-gradle,debugAndroidTestCompileClasspath,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath org.jetbrains:annotations:23.0.0=_internal-unified-test-platform-android-device-provider-ddmlib,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath +org.jitsi:webrtc:124.0.0=debugAndroidTestCompileClasspath,debugCompileClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseRuntimeClasspath,releaseUnitTestCompileClasspath,releaseUnitTestRuntimeClasspath org.ow2.asm:asm-analysis:9.2=androidJacocoAnt org.ow2.asm:asm-commons:9.2=androidJacocoAnt org.ow2.asm:asm-tree:9.2=androidJacocoAnt diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 24c61bced..cd4c656c1 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,12 @@ + + + + + + + @@ -8,6 +15,13 @@ + + + + + + + diff --git a/app/ios/AriesBifold/AppDelegate.mm b/app/ios/AriesBifold/AppDelegate.mm index 2403589d3..31516077f 100644 --- a/app/ios/AriesBifold/AppDelegate.mm +++ b/app/ios/AriesBifold/AppDelegate.mm @@ -1,5 +1,6 @@ #import "AppDelegate.h" +#import #import #import #import @@ -11,6 +12,9 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + // allows background WebRTC + [WebRTCModuleOptions sharedInstance].enableMultitaskingCameraAccess = YES; + [FIRApp configure]; self.moduleName = @"BCWallet"; // You can add your custom initial props in the dictionary below. @@ -20,7 +24,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( // Because certain file operations can reset resource values, we // excluded file’s resource values each time the application starts. [self excludeDotAFJFolderFromBackup]; - + return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/app/ios/AriesBifold/Info.plist b/app/ios/AriesBifold/Info.plist index 5ecad104b..94cbcab0e 100644 --- a/app/ios/AriesBifold/Info.plist +++ b/app/ios/AriesBifold/Info.plist @@ -64,6 +64,10 @@ + NSBluetoothAlwaysUsageDescription + $(PRODUCT_NAME) needs access to Bluetooth to manage audio routing during calls + NSBluetoothPeripheralUsageDescription + $(PRODUCT_NAME) needs access to Bluetooth to connect to audio devices during calls NSCameraUsageDescription Camera used for QR Code scanning and video calls NSFaceIDUsageDescription @@ -84,6 +88,8 @@ UIBackgroundModes remote-notification + audio + voip UILaunchStoryboardName LaunchScreen diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 1a1a8a375..2f48af9ca 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -153,7 +153,9 @@ PODS: - React - React-callinvoker - React-Core + - JitsiWebRTC (124.0.2) - libevent (2.1.12) + - Mute (0.6.1) - nanopb (2.30908.0): - nanopb/decode (= 2.30908.0) - nanopb/encode (= 2.30908.0) @@ -1070,6 +1072,12 @@ PODS: - glog - RCT-Folly (= 2022.05.16.00) - React-Core + - react-native-volume-manager (1.10.0): + - Mute + - React-Core + - react-native-webrtc (124.0.6): + - JitsiWebRTC (~> 124.0.0) + - React-Core - react-native-webview (13.10.7): - glog - RCT-Folly (= 2022.05.16.00) @@ -1241,6 +1249,8 @@ PODS: - React-jsi (= 0.73.11) - React-logger (= 0.73.11) - React-perflogger (= 0.73.11) + - ReactNativeIncallManager (4.2.1): + - React-Core - RNArgon2 (2.0.1): - CatCrypto - React-Core @@ -1349,6 +1359,8 @@ DEPENDENCIES: - react-native-splash-screen (from `../node_modules/react-native-splash-screen`) - react-native-tcp-socket (from `../node_modules/react-native-tcp-socket`) - react-native-video (from `../node_modules/react-native-video`) + - react-native-volume-manager (from `../node_modules/react-native-volume-manager`) + - react-native-webrtc (from `../node_modules/react-native-webrtc`) - react-native-webview (from `../node_modules/react-native-webview`) - React-nativeconfig (from `../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) @@ -1370,6 +1382,7 @@ DEPENDENCIES: - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - ReactNativeIncallManager (from `../node_modules/react-native-incall-manager`) - RNArgon2 (from `../node_modules/react-native-argon2`) - "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)" - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" @@ -1404,7 +1417,9 @@ SPEC REPOS: - GoogleAppMeasurement - GoogleDataTransport - GoogleUtilities + - JitsiWebRTC - libevent + - Mute - nanopb - PromisesObjC - SDWebImage @@ -1498,6 +1513,10 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-tcp-socket" react-native-video: :path: "../node_modules/react-native-video" + react-native-volume-manager: + :path: "../node_modules/react-native-volume-manager" + react-native-webrtc: + :path: "../node_modules/react-native-webrtc" react-native-webview: :path: "../node_modules/react-native-webview" React-nativeconfig: @@ -1540,6 +1559,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/utils" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + ReactNativeIncallManager: + :path: "../node_modules/react-native-incall-manager" RNArgon2: :path: "../node_modules/react-native-argon2" RNCAsyncStorage: @@ -1601,7 +1622,9 @@ SPEC CHECKSUMS: GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 hermes-engine: d992945b77c506e5164e6a9a77510c9d57472c59 indy-vdr: aada31078a9ed270dd618fadb4cf69bcdc333d68 + JitsiWebRTC: b47805ab5668be38e7ee60e2258f49badfe8e1d0 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + Mute: 20135a96076f140cc82bfc8b810e2d6150d8ec7e nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: cd21f1661364f975ae76b3308167ad66b09f53f5 @@ -1637,6 +1660,8 @@ SPEC CHECKSUMS: react-native-splash-screen: 95994222cc95c236bd3cdc59fe45ed5f27969594 react-native-tcp-socket: ae8abcfebc071216302a09d9ed1e375d4e877484 react-native-video: d74d94fbaeee3c0d8f6570173289f43fe210066f + react-native-volume-manager: d9d2863a2374420af89c89662333ea6adf506988 + react-native-webrtc: 96fdff9e3a942ed88cafe01898da1c93fd628957 react-native-webview: f802f655c8446404bb0c134da9335a8cf667e8cb React-nativeconfig: 8fd29a35a3e4e8c37682d976667663d834ba6165 React-NativeModulesApple: 83d7077877f8eda8e1b6055b3f8f16f7db8463b5 @@ -1658,6 +1683,7 @@ SPEC CHECKSUMS: React-runtimescheduler: 398069b748d97567cc7585cc9a97284ad19d72fa React-utils: e8549669b504c18929b2e9aa4d87657e530a91a4 ReactCommon: 9c38e8797dc2ac72edf63cd18cf450d918575666 + ReactNativeIncallManager: dccd3e7499caa3bb73d3acfedf4fb0360f1a87d5 RNArgon2: 708e188b7a4d4ec8baf62463927c47abef453a94 RNCAsyncStorage: 9350c2956f996b3ff1ac7cfdb50901c113a27640 RNCClipboard: f6679d470d0da2bce2a37b0af7b9e0bf369ecda5 diff --git a/app/package.json b/app/package.json index 0ef2aea91..1e9267012 100644 --- a/app/package.json +++ b/app/package.json @@ -68,6 +68,7 @@ "@hyperledger/aries-askar-react-native": "0.2.3", "@hyperledger/indy-vdr-react-native": "0.2.2", "@hyperledger/indy-vdr-shared": "0.2.2", + "@pexip/infinity-api": "~19.1.2", "@react-native-async-storage/async-storage": "~1.22.3", "@react-native-clipboard/clipboard": "~1.16.3", "@react-native-community/netinfo": "~11.3.3", @@ -109,6 +110,7 @@ "react-native-get-random-values": "~1.8.0", "react-native-gifted-chat": "*", "react-native-inappbrowser-reborn": "~3.7.0", + "react-native-incall-manager": "~4.2.1", "react-native-keychain": "~8.1.3", "react-native-localize": "~2.2.6", "react-native-logs": "~5.1.0", @@ -127,6 +129,8 @@ "react-native-vector-icons": "~10.0.3", "react-native-video": "~6.16.1", "react-native-vision-camera": "~4.3.2", + "react-native-volume-manager": "~1.10.0", + "react-native-webrtc": "~124.0.6", "react-native-webview": "~13.10.7", "reflect-metadata": "~0.1.14", "rxjs": "~7.8.2", diff --git a/app/src/assets/img/mountains-circle.svg b/app/src/assets/img/mountains-circle.svg new file mode 100644 index 000000000..65d8edce9 --- /dev/null +++ b/app/src/assets/img/mountains-circle.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/bcsc-theme/api/client.ts b/app/src/bcsc-theme/api/client.ts index 1be7c8207..03826ee64 100644 --- a/app/src/bcsc-theme/api/client.ts +++ b/app/src/bcsc-theme/api/client.ts @@ -42,6 +42,7 @@ interface BCSCEndpoints { token: string credential: string evidence: string + video: string } class BCSCService { @@ -95,6 +96,7 @@ class BCSCService { token: `${this.baseURL}/device/token`, credential: `${this.baseURL}/credentials/v1/person`, evidence: `${this.baseURL}/evidence`, + video: `${this.baseURL}/video`, } // Add interceptors @@ -158,8 +160,9 @@ class BCSCService { savedServices: response.data['saved_services_endpoint'], token: response.data['token_endpoint'], credential: response.data['credential_endpoint'], - // TODO(bm): request backend team to add evidence endpoint to the response + // TODO(bm): request backend team to add evidence and video endpoints to the response evidence: `${this.baseURL}/evidence`, + video: `${this.baseURL}/video`, } } diff --git a/app/src/bcsc-theme/api/hooks/useApi.tsx b/app/src/bcsc-theme/api/hooks/useApi.tsx index c0f7a02a4..6140e9a60 100644 --- a/app/src/bcsc-theme/api/hooks/useApi.tsx +++ b/app/src/bcsc-theme/api/hooks/useApi.tsx @@ -8,6 +8,7 @@ import useUserApi from './useUserApi' import useEvidenceApi from './useEvidenceApi' import useMetadataApi from './useMetadataApi' import useJwksApi from './useJwksApi' +import useVideoCallApi from './useVideoCallApi' const useApi = () => { const config = useConfigApi() @@ -19,6 +20,7 @@ const useApi = () => { const evidence = useEvidenceApi() const metadata = useMetadataApi() const jwks = useJwksApi() + const video = useVideoCallApi() return useMemo( () => ({ @@ -31,8 +33,9 @@ const useApi = () => { evidence, metadata, jwks, + video, }), - [config, pairing, registration, authorization, token, user, evidence, metadata, jwks] + [config, pairing, registration, authorization, token, user, evidence, metadata, jwks, video] ) } diff --git a/app/src/bcsc-theme/api/hooks/useAuthorizationApi.tsx b/app/src/bcsc-theme/api/hooks/useAuthorizationApi.tsx index 911cf3d61..9264c8f34 100644 --- a/app/src/bcsc-theme/api/hooks/useAuthorizationApi.tsx +++ b/app/src/bcsc-theme/api/hooks/useAuthorizationApi.tsx @@ -1,9 +1,9 @@ +import { ProvinceCode } from '@/bcsc-theme/utils/address-utils' +import { isAxiosError } from 'axios' import { useCallback, useMemo } from 'react' +import { createDeviceSignedJWT } from 'react-native-bcsc-core' import apiClient from '../client' import { withAccount } from './withAccountGuard' -import { createDeviceSignedJWT } from 'react-native-bcsc-core' -import { isAxiosError } from 'axios' -import { ProvinceCode } from '@/bcsc-theme/utils/address-utils' const INVALID_REGISTRATION_REQUEST = 'invalid_registration_request' @@ -31,7 +31,7 @@ export interface VerifyUnknownBCSCResponseData { expires_in: number } -interface AuthorizeDeviceUnknownBCSCConfig { +export interface AuthorizeDeviceUnknownBCSCConfig { firstName: string lastName: string birthdate: string diff --git a/app/src/bcsc-theme/api/hooks/useEvidenceApi.tsx b/app/src/bcsc-theme/api/hooks/useEvidenceApi.tsx index 0b33a1222..7759c1a7a 100644 --- a/app/src/bcsc-theme/api/hooks/useEvidenceApi.tsx +++ b/app/src/bcsc-theme/api/hooks/useEvidenceApi.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo } from 'react' import { useStore } from '@bifold/core' import apiClient from '../client' import { withAccount } from './withAccountGuard' -import { createEvidenceRequestJWT } from 'react-native-bcsc-core' +import { createPreVerificationJWT } from 'react-native-bcsc-core' import { BCState } from '@/store' export interface VerificationPrompt { @@ -111,7 +111,7 @@ const useEvidenceApi = () => { // This needs ot be called for the process to start const createVerificationRequest = useCallback(async (): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.post( `${apiClient.endpoints.evidence}/v1/verifications`, null, @@ -130,7 +130,7 @@ const useEvidenceApi = () => { const uploadPhotoEvidenceMetadata = useCallback( async (payload: VerificationPhotoUploadPayload): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.post( `${apiClient.endpoints.evidence}/v1/photos`, payload, @@ -149,7 +149,7 @@ const useEvidenceApi = () => { const uploadVideoEvidenceMetadata = useCallback( async (payload: VerificationVideoUploadPayload): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.post( `${apiClient.endpoints.evidence}/v1/videos`, payload, @@ -172,7 +172,7 @@ const useEvidenceApi = () => { payload: SendVerificationPayload ): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.put( `${apiClient.endpoints.evidence}/v1/verifications/${verificationRequestId}`, payload, @@ -192,7 +192,7 @@ const useEvidenceApi = () => { const getVerificationRequestPrompts = useCallback( async (verificationRequestId: string): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.get( `${apiClient.endpoints.evidence}/v1/verifications/${verificationRequestId}/prompts`, { @@ -211,7 +211,7 @@ const useEvidenceApi = () => { const getVerificationRequestStatus = useCallback( async (verificationRequestId: string): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.get( `${apiClient.endpoints.evidence}/v1/verifications/${verificationRequestId}`, { @@ -233,7 +233,7 @@ const useEvidenceApi = () => { const cancelVerificationRequest = useCallback( async (verificationRequestId: string): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.delete( `${apiClient.endpoints.evidence}/v1/verifications/${verificationRequestId}`, { @@ -252,7 +252,7 @@ const useEvidenceApi = () => { const createEmailVerification = useCallback( async (email: string): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.post( `${apiClient.endpoints.evidence}/v1/emails`, { email_address: email }, @@ -272,7 +272,7 @@ const useEvidenceApi = () => { const sendEmailVerificationCode = useCallback( async (code: string, emailAddressId: string): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.put( `${apiClient.endpoints.evidence}/v1/emails/${emailAddressId}`, { @@ -294,7 +294,7 @@ const useEvidenceApi = () => { const uploadPhotoEvidenceBinary = useCallback( async (url: string, binaryData: any): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.put(url, binaryData, { headers: { Authorization: `Bearer ${token}`, @@ -312,7 +312,7 @@ const useEvidenceApi = () => { const uploadVideoEvidenceBinary = useCallback( async (url: string, binaryData: any): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.put(url, binaryData, { headers: { Authorization: `Bearer ${token}`, @@ -330,7 +330,7 @@ const useEvidenceApi = () => { const sendEvidenceMetadata = useCallback( async (payload: EvidenceMetadataPayload): Promise => { return withAccount(async (account) => { - const token = await createEvidenceRequestJWT(_getDeviceCode(), account.clientID) + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) const { data } = await apiClient.post( `${apiClient.endpoints.evidence}/v1/documents`, payload, diff --git a/app/src/bcsc-theme/api/hooks/useTokens.tsx b/app/src/bcsc-theme/api/hooks/useTokens.tsx index e3a9404cc..ca34fcc58 100644 --- a/app/src/bcsc-theme/api/hooks/useTokens.tsx +++ b/app/src/bcsc-theme/api/hooks/useTokens.tsx @@ -1,8 +1,8 @@ +import { getDeviceCountFromIdToken } from '@/bcsc-theme/utils/get-device-count' import { useCallback, useMemo } from 'react' import { getDeviceCodeRequestBody } from 'react-native-bcsc-core' import apiClient, { TokenStatusResponseDataWithDeviceCount } from '../client' import { withAccount } from './withAccountGuard' -import { getDeviceCountFromIdToken } from '@/bcsc-theme/utils/get-device-count' export interface TokenStatusResponseData { access_token: string @@ -23,7 +23,6 @@ const useTokenApi = () => { return withAccount(async (account) => { const { clientID, issuer } = account const body = await getDeviceCodeRequestBody(deviceCode, clientID, issuer, confirmationCode) - apiClient.logger.info(`Device code body: ${body}`) const { data } = await apiClient.post(apiClient.endpoints.token, body, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, skipBearerAuth: true, diff --git a/app/src/bcsc-theme/api/hooks/useVideoCallApi.tsx b/app/src/bcsc-theme/api/hooks/useVideoCallApi.tsx new file mode 100644 index 000000000..c87bf41de --- /dev/null +++ b/app/src/bcsc-theme/api/hooks/useVideoCallApi.tsx @@ -0,0 +1,219 @@ +import { useCallback, useMemo } from 'react' +import { useStore } from '@bifold/core' +import apiClient from '../client' +import { withAccount } from './withAccountGuard' +import { createPreVerificationJWT } from 'react-native-bcsc-core' +import { BCState } from '@/store' + +type SessionStatusType = 'session_granted' | 'session_not_granted' | 'session_failed' | 'session_ended' +type CallStatusType = + | 'call_ringing' + | 'call_media_pending' + | 'call_in_call' + | 'call_ended' + | 'call_error' + | 'call_dropped' + | 'call_reconnected' + +export interface VideoSession { + client_id: string + device_code: string + session_id: string + destination_host: string + room_name: string + room_alias: string + guest_pin: string + queue_position: number + status: SessionStatusType + status_date: number // seconds from epoch + created_date: number // seconds from epoch +} + +export interface VideoCall { + session_id: string + call_id: string + client_call_id: string + status: CallStatusType + status_date: number // seconds from epoch +} + +export interface VideoDestination { + max_active_sessions?: number + max_inactive_seconds?: number + number_of_agents?: number + destination_name: string + destination_priority: number + destination_host?: string +} + +export type VideoDestinations = VideoDestination[] + +export interface ServicePeriod { + start_day: string // e.g. "MONDAY" + end_day: string // e.g. "MONDAY" + start_time: string // e.g. "05:00" + end_time: string // e.g. "23:59" +} + +export interface ServiceHours { + time_zone: string + regular_service_periods: ServicePeriod[] + service_unavailable_periods: ServicePeriod[] +} + +const useVideoCallApi = () => { + const [store] = useStore() + + const _getDeviceCode = useCallback(() => { + const code = store.bcsc.deviceCode + if (!code) throw new Error('Device code is missing. Re install the app and try again.') + return code + }, [store.bcsc.deviceCode]) + + const createVideoSession = useCallback(async (): Promise => { + return withAccount(async (account) => { + const deviceCode = _getDeviceCode() + const body = { client_id: account.clientID, device_code: deviceCode } + const token = await createPreVerificationJWT(deviceCode, account.clientID) + const { data } = await apiClient.post(`${apiClient.endpoints.video}/v2/sessions/`, body, { + headers: { + Authorization: `Bearer ${token}`, + }, + skipBearerAuth: true, + }) + return data + }) + }, [_getDeviceCode]) + + const updateVideoSessionStatus = useCallback( + async (sessionId: string, status: SessionStatusType): Promise => { + return withAccount(async (account) => { + const body = { status } + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) + const { data } = await apiClient.put( + `${apiClient.endpoints.video}/v2/sessions/${sessionId}`, + body, + { + headers: { + Authorization: `Bearer ${token}`, + }, + skipBearerAuth: true, + } + ) + return data + }) + }, + [_getDeviceCode] + ) + + const createVideoCall = useCallback( + async (sessionId: string, clientCallId: string, status: CallStatusType = 'call_ringing'): Promise => { + return withAccount(async (account) => { + const body = { + session_id: sessionId, + status: status, + client_call_id: clientCallId, + } + + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) + const { data } = await apiClient.post( + `${apiClient.endpoints.video}/v2/sessions/${sessionId}/calls/`, + body, + { + headers: { + Authorization: `Bearer ${token}`, + }, + skipBearerAuth: true, + } + ) + return data + }) + }, + [_getDeviceCode] + ) + + const updateVideoCallStatus = useCallback( + async (sessionId: string, clientCallId: string, status: CallStatusType): Promise => { + return withAccount(async (account) => { + const body = { status, client_call_id: clientCallId } + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) + const { data } = await apiClient.put( + `${apiClient.endpoints.video}/v2/sessions/${sessionId}/calls/${clientCallId}`, + body, + { + headers: { + Authorization: `Bearer ${token}`, + }, + skipBearerAuth: true, + } + ) + return data + }) + }, + [_getDeviceCode] + ) + + const endVideoSession = useCallback( + async (sessionId: string): Promise => { + return withAccount(async (account) => { + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) + await apiClient.delete(`${apiClient.endpoints.video}/v2/sessions/${sessionId}`, { + headers: { + Authorization: `Bearer ${token}`, + }, + skipBearerAuth: true, + }) + }) + }, + [_getDeviceCode] + ) + + const getVideoDestinations = useCallback(async (): Promise => { + return withAccount(async (account) => { + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) + const { data } = await apiClient.get(`${apiClient.endpoints.video}/v2/destinations`, { + headers: { + Authorization: `Bearer ${token}`, + }, + skipBearerAuth: true, + }) + return data + }) + }, [_getDeviceCode]) + + const getServiceHours = useCallback(async (): Promise => { + return withAccount(async (account) => { + const token = await createPreVerificationJWT(_getDeviceCode(), account.clientID) + const { data } = await apiClient.get(`${apiClient.endpoints.video}/v2/service_hours`, { + headers: { + Authorization: `Bearer ${token}`, + }, + skipBearerAuth: true, + }) + return data + }) + }, [_getDeviceCode]) + + return useMemo( + () => ({ + createVideoSession, + updateVideoSessionStatus, + createVideoCall, + updateVideoCallStatus, + endVideoSession, + getVideoDestinations, + getServiceHours, + }), + [ + createVideoSession, + updateVideoSessionStatus, + createVideoCall, + updateVideoCallStatus, + endVideoSession, + getVideoDestinations, + getServiceHours, + ] + ) +} + +export default useVideoCallApi diff --git a/app/src/bcsc-theme/components/AppBanner.tsx b/app/src/bcsc-theme/components/AppBanner.tsx index 3573c8ced..e351f7b37 100644 --- a/app/src/bcsc-theme/components/AppBanner.tsx +++ b/app/src/bcsc-theme/components/AppBanner.tsx @@ -1,6 +1,6 @@ -import { View, StyleSheet, TouchableOpacity } from 'react-native' -import { useTheme, ThemedText, testIdWithKey } from '@bifold/core' +import { ThemedText, testIdWithKey, useTheme } from '@bifold/core' import React, { useEffect, useState } from 'react' +import { StyleSheet, TouchableOpacity, View } from 'react-native' import Icon from 'react-native-vector-icons/MaterialCommunityIcons' export interface AppBannerSectionProps { title: string @@ -50,7 +50,9 @@ export const AppBannerSection: React.FC = ({ title, type, backgroundColor: ColorPalette.brand.primary, flexDirection: 'row', alignItems: 'center', + flexWrap: 'wrap', padding: Spacing.md, + flexShrink: 1, }, icon: { marginRight: Spacing.md, @@ -107,7 +109,10 @@ export const AppBannerSection: React.FC = ({ title, type, /> {title} diff --git a/app/src/bcsc-theme/components/MaskedCamera.tsx b/app/src/bcsc-theme/components/MaskedCamera.tsx index ab055a3d8..c5aed87de 100644 --- a/app/src/bcsc-theme/components/MaskedCamera.tsx +++ b/app/src/bcsc-theme/components/MaskedCamera.tsx @@ -157,10 +157,10 @@ const MaskedCamera = ({ /> - + {cameraLabel} - + {cameraInstructions} diff --git a/app/src/bcsc-theme/features/verify/EnterBirthdateScreen.tsx b/app/src/bcsc-theme/features/verify/EnterBirthdateScreen.tsx index abd6f0d47..24805aebb 100644 --- a/app/src/bcsc-theme/features/verify/EnterBirthdateScreen.tsx +++ b/app/src/bcsc-theme/features/verify/EnterBirthdateScreen.tsx @@ -16,12 +16,12 @@ import DatePicker from 'react-native-date-picker' import { SafeAreaView } from 'react-native-safe-area-context' import useApi from '@/bcsc-theme/api/hooks/useApi' +import { BCSCCardType } from '@/bcsc-theme/types/cards' import { BCSCScreens, BCSCVerifyIdentityStackParams } from '@/bcsc-theme/types/navigators' import { BCThemeNames } from '@/constants' import { BCDispatchAction, BCState } from '@/store' import { CommonActions } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' -import { BCSCCardType } from '@/bcsc-theme/types/cards' type EnterBirthdateScreenProps = { navigation: StackNavigationProp @@ -99,9 +99,6 @@ const EnterBirthdateScreen: React.FC = ({ navigation } finally { setLoading(false) } - - // if successful, navigation reset to setup steps screen - // if not successful, navigate to mismatch screen }, [dispatch, date, navigation, authorization, store.bcsc.serial, logger, store.bcsc.cardType]) return ( @@ -120,6 +117,8 @@ const EnterBirthdateScreen: React.FC = ({ navigation theme={themeName === BCThemeNames.BCSC ? 'dark' : 'light'} mode={'date'} date={date} + // https://github.com/henninghall/react-native-date-picker/issues/724#issuecomment-1850045253 + timeZoneOffsetInMinutes={date.getTimezoneOffset()} onDateChange={setDate} onStateChange={setPickerState} /> diff --git a/app/src/bcsc-theme/features/verify/send-video/PhotoInstructionsScreen.tsx b/app/src/bcsc-theme/features/verify/PhotoInstructionsScreen.tsx similarity index 85% rename from app/src/bcsc-theme/features/verify/send-video/PhotoInstructionsScreen.tsx rename to app/src/bcsc-theme/features/verify/PhotoInstructionsScreen.tsx index 8f95a5421..49bb6c8b3 100644 --- a/app/src/bcsc-theme/features/verify/send-video/PhotoInstructionsScreen.tsx +++ b/app/src/bcsc-theme/features/verify/PhotoInstructionsScreen.tsx @@ -4,14 +4,17 @@ import SelfieImage from '@assets/img/selfie_example.png' import { Image, StyleSheet, View } from 'react-native' import { BCSCScreens, BCSCVerifyIdentityStackParams } from '@/bcsc-theme/types/navigators' import { StackNavigationProp } from '@react-navigation/stack' +import { RouteProp } from '@react-navigation/native' const SELFIE_IMAGE = Image.resolveAssetSource(SelfieImage).uri type PhotoInstructionsScreenProps = { navigation: StackNavigationProp + route: RouteProp } -const PhotoInstructionsScreen = ({ navigation }: PhotoInstructionsScreenProps) => { +const PhotoInstructionsScreen = ({ navigation, route }: PhotoInstructionsScreenProps) => { + const { forLiveCall } = route.params const { ColorPalette, Spacing } = useTheme() const styles = StyleSheet.create({ @@ -69,7 +72,12 @@ const PhotoInstructionsScreen = ({ navigation }: PhotoInstructionsScreenProps) = buttonType={ButtonType.Primary} title={'Take Photo of Face'} onPress={() => { - navigation.navigate(BCSCScreens.TakePhoto, { deviceSide: 'front', cameraInstructions: '', cameraLabel: '' }) + navigation.navigate(BCSCScreens.TakePhoto, { + deviceSide: 'front', + cameraInstructions: '', + cameraLabel: '', + forLiveCall, + }) }} testID={'TakePhotoButton'} accessibilityLabel={'Take Photo of Face'} diff --git a/app/src/bcsc-theme/features/verify/send-video/PhotoReviewScreen.tsx b/app/src/bcsc-theme/features/verify/PhotoReviewScreen.tsx similarity index 74% rename from app/src/bcsc-theme/features/verify/send-video/PhotoReviewScreen.tsx rename to app/src/bcsc-theme/features/verify/PhotoReviewScreen.tsx index 0be320e14..7fbf0b4d0 100644 --- a/app/src/bcsc-theme/features/verify/send-video/PhotoReviewScreen.tsx +++ b/app/src/bcsc-theme/features/verify/PhotoReviewScreen.tsx @@ -3,24 +3,20 @@ import { BCDispatchAction, BCState } from '@/store' import { BCSCScreens, BCSCVerifyIdentityStackParams } from '@bcsc-theme/types/navigators' import { getPhotoMetadata } from '@bcsc-theme/utils/file-info' import { TOKENS, useServices, useStore, useTheme } from '@bifold/core' -import { CommonActions } from '@react-navigation/native' +import { CommonActions, RouteProp } from '@react-navigation/native' import { StackNavigationProp } from '@react-navigation/stack' import { StyleSheet } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' type PhotoReviewScreenProps = { navigation: StackNavigationProp - route: { - params: { - photoPath: string - } - } + route: RouteProp } const PhotoReviewScreen = ({ navigation, route }: PhotoReviewScreenProps) => { - const { ColorPalette, Spacing } = useTheme() + const { ColorPalette } = useTheme() const [, dispatch] = useStore() - const { photoPath } = route.params + const { photoPath, forLiveCall } = route.params const [logger] = useServices([TOKENS.UTIL_LOGGER]) if (!photoPath) { @@ -33,20 +29,6 @@ const PhotoReviewScreen = ({ navigation, route }: PhotoReviewScreenProps) => { flexGrow: 1, backgroundColor: ColorPalette.brand.primaryBackground, }, - contentContainer: { - flexGrow: 1, - }, - controlsContainer: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - padding: Spacing.md, - backgroundColor: ColorPalette.notification.popupOverlay, - }, - secondButton: { - marginTop: Spacing.sm, - }, }) const onPressUse = async () => { @@ -55,6 +37,21 @@ const PhotoReviewScreen = ({ navigation, route }: PhotoReviewScreenProps) => { dispatch({ type: BCDispatchAction.SAVE_PHOTO, payload: [{ photoPath, photoMetadata }] }) + if (forLiveCall) { + navigation.dispatch( + CommonActions.reset({ + index: 3, + routes: [ + { name: BCSCScreens.SetupSteps }, + { name: BCSCScreens.VerificationMethodSelection }, + { name: BCSCScreens.PhotoInstructions, params: { forLiveCall: true } }, + { name: BCSCScreens.StartCall }, + ], + }) + ) + return + } + navigation.dispatch( CommonActions.reset({ index: 2, @@ -67,7 +64,6 @@ const PhotoReviewScreen = ({ navigation, route }: PhotoReviewScreenProps) => { ) } catch (error) { logger.error(`Error saving photo: ${error}`) - // TODO: Handle error, e.g., show an alert or log the error } } diff --git a/app/src/bcsc-theme/features/verify/send-video/TakePhotoScreen.tsx b/app/src/bcsc-theme/features/verify/TakePhotoScreen.tsx similarity index 82% rename from app/src/bcsc-theme/features/verify/send-video/TakePhotoScreen.tsx rename to app/src/bcsc-theme/features/verify/TakePhotoScreen.tsx index 9249d41f7..a9ab34c54 100644 --- a/app/src/bcsc-theme/features/verify/send-video/TakePhotoScreen.tsx +++ b/app/src/bcsc-theme/features/verify/TakePhotoScreen.tsx @@ -6,12 +6,14 @@ import { StyleSheet } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' import MaskedCamera from '@/bcsc-theme/components/MaskedCamera' import CircularMask from '@/bcsc-theme/components/CircularMask' +import { RouteProp } from '@react-navigation/native' type PhotoInstructionsScreenProps = { navigation: StackNavigationProp + route: RouteProp } -const TakePhotoScreen = ({ navigation }: PhotoInstructionsScreenProps) => { +const TakePhotoScreen = ({ navigation, route }: PhotoInstructionsScreenProps) => { const styles = StyleSheet.create({ pageContainer: { flex: 1, @@ -23,6 +25,7 @@ const TakePhotoScreen = ({ navigation }: PhotoInstructionsScreenProps) => { // Navigate to photo review screen with the photo data navigation.navigate(BCSCScreens.PhotoReview, { photoPath: path, + forLiveCall: route.params.forLiveCall, }) } diff --git a/app/src/bcsc-theme/features/verify/VerificationMethodSelectionScreen.tsx b/app/src/bcsc-theme/features/verify/VerificationMethodSelectionScreen.tsx index d148cf1ea..4300a22a2 100644 --- a/app/src/bcsc-theme/features/verify/VerificationMethodSelectionScreen.tsx +++ b/app/src/bcsc-theme/features/verify/VerificationMethodSelectionScreen.tsx @@ -1,13 +1,14 @@ import useApi from '@/bcsc-theme/api/hooks/useApi' +import { BCSCCardType } from '@/bcsc-theme/types/cards' +import { checkIfWithinServiceHours, formatServiceHours } from '@/bcsc-theme/utils/serviceHoursFormatter' import { BCDispatchAction, BCState } from '@/store' import { BCSCScreens, BCSCVerifyIdentityStackParams } from '@bcsc-theme/types/navigators' -import { ThemedText, useStore, useTheme } from '@bifold/core' +import { ThemedText, TOKENS, useServices, useStore, useTheme } from '@bifold/core' import { StackNavigationProp } from '@react-navigation/stack' -import { useState } from 'react' +import { useCallback, useState } from 'react' import { StyleSheet } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' import VerifyMethodActionButton from './components/VerifyMethodActionButton' -import { BCSCCardType } from '@/bcsc-theme/types/cards' type VerificationMethodSelectionScreenProps = { navigation: StackNavigationProp @@ -16,8 +17,10 @@ type VerificationMethodSelectionScreenProps = { const VerificationMethodSelectionScreen = ({ navigation }: VerificationMethodSelectionScreenProps) => { const { ColorPalette, Spacing } = useTheme() const [store, dispatch] = useStore() - const [loading, setLoading] = useState(false) - const { evidence } = useApi() + const [sendVideoLoading, setSendVideoLoading] = useState(false) + const [liveCallLoading, setLiveCallLoading] = useState(false) + const { evidence, video: videoCallApi } = useApi() + const [logger] = useServices([TOKENS.UTIL_LOGGER]) const styles = StyleSheet.create({ pageContainer: { @@ -28,7 +31,7 @@ const VerificationMethodSelectionScreen = ({ navigation }: VerificationMethodSel const handlePressSendVideo = async () => { try { - setLoading(true) + setSendVideoLoading(true) const { sha256, id, prompts } = await evidence.createVerificationRequest() dispatch({ type: BCDispatchAction.UPDATE_VERIFICATION_REQUEST, payload: [{ sha256, id }] }) dispatch({ type: BCDispatchAction.UPDATE_VIDEO_PROMPTS, payload: [prompts] }) @@ -37,10 +40,56 @@ const VerificationMethodSelectionScreen = ({ navigation }: VerificationMethodSel // TODO: Handle error, e.g., show an alert or log the error return } finally { - setLoading(false) + setSendVideoLoading(false) } } + const handlePressLiveCall = useCallback(async () => { + try { + setLiveCallLoading(true) + + const [destinations, serviceHours] = await Promise.all([ + videoCallApi.getVideoDestinations(), + videoCallApi.getServiceHours(), + ]) + + const formattedHours = formatServiceHours(serviceHours) + + // TODO (bm): Look for prod queue(s) depending on environment + const availableDestination = destinations.find( + (dest) => dest.destination_name === 'Test Harness Queue Destination' + ) + + if (!availableDestination) { + navigation.navigate(BCSCScreens.CallBusyOrClosed, { + busy: true, + formattedHours, + }) + return + } + + const isWithinServiceHours = checkIfWithinServiceHours(serviceHours) + + if (!isWithinServiceHours) { + navigation.navigate(BCSCScreens.CallBusyOrClosed, { + busy: false, + formattedHours, + }) + return + } + + navigation.navigate(BCSCScreens.BeforeYouCall, { formattedHours }) + } catch (error) { + logger.error('Error checking service availability:', error as Error) + navigation.navigate(BCSCScreens.CallBusyOrClosed, { + busy: false, + formattedHours: 'Unavailable', + }) + } finally { + setLiveCallLoading(false) + } + }, [videoCallApi, logger, navigation]) + return ( - { // Do not show video call option for "Other" card type ie: dual identification cards store.bcsc.cardType !== BCSCCardType.Other ? ( @@ -67,9 +115,10 @@ const VerificationMethodSelectionScreen = ({ navigation }: VerificationMethodSel title={'Video call'} description={`We will verify your identity during a video call.`} icon={'video'} - onPress={() => null} + onPress={handlePressLiveCall} style={{ borderBottomWidth: 0 }} - disabled={loading} + loading={liveCallLoading} + disabled={liveCallLoading || sendVideoLoading} /> ) : null @@ -80,7 +129,7 @@ const VerificationMethodSelectionScreen = ({ navigation }: VerificationMethodSel description={`Find out where to go and what to bring.`} icon={'account'} onPress={() => navigation.navigate(BCSCScreens.VerifyInPerson)} - disabled={loading} + disabled={liveCallLoading || sendVideoLoading} /> ) diff --git a/app/src/bcsc-theme/features/verify/VerifyIdentityStack.tsx b/app/src/bcsc-theme/features/verify/VerifyIdentityStack.tsx index ec07ef071..c582b995e 100644 --- a/app/src/bcsc-theme/features/verify/VerifyIdentityStack.tsx +++ b/app/src/bcsc-theme/features/verify/VerifyIdentityStack.tsx @@ -1,39 +1,44 @@ +import createHelpHeaderButton from '@/bcsc-theme/components/HelpHeaderButton' +import { createWebviewHeaderBackButton } from '@/bcsc-theme/components/WebViewBackButton' import { BCSCScreens, BCSCVerifyIdentityStackParams } from '@/bcsc-theme/types/navigators' +import { HelpCentreUrl } from '@/constants' import { testIdWithKey, useDefaultStackOptions, useTheme } from '@bifold/core' import { createStackNavigator } from '@react-navigation/stack' -import SetupStepsScreen from './SetupStepsScreen' +import WebViewScreen from '../webview/WebViewScreen' +import EnterBirthdateScreen from './EnterBirthdateScreen' import IdentitySelectionScreen from './IdentitySelectionScreen' -import SerialInstructionsScreen from './SerialInstructionsScreen' import ManualSerialScreen from './ManualSerialScreen' +import MismatchedSerialScreen from './MismatchedSerialScreen' +import PhotoInstructionsScreen from './PhotoInstructionsScreen' +import PhotoReviewScreen from './PhotoReviewScreen' +import { ResidentialAddressScreen } from './ResidentialAddressScreen' import ScanSerialScreen from './ScanSerialScreen' -import EnterBirthdateScreen from './EnterBirthdateScreen' +import SerialInstructionsScreen from './SerialInstructionsScreen' +import SetupStepsScreen from './SetupStepsScreen' +import TakePhotoScreen from './TakePhotoScreen' import VerificationMethodSelectionScreen from './VerificationMethodSelectionScreen' -import VerifyInPersonScreen from './in-person/VerifyInPersonScreen' -import MismatchedSerialScreen from './MismatchedSerialScreen' import VerificationSuccessScreen from './VerificationSuccessScreen' +import EmailConfirmationScreen from './email/EmailConfirmationScreen' +import EnterEmailScreen from './email/EnterEmailScreen' +import VerifyInPersonScreen from './in-person/VerifyInPersonScreen' +import BeforeYouCallScreen from './live-call/BeforeYouCallScreen' +import CallBusyOrClosedScreen from './live-call/CallBusyOrClosedScreen' +import LiveCallScreen from './live-call/LiveCallScreen' +import StartCallScreen from './live-call/StartCallScreen' +import VerifyNotCompleteScreen from './live-call/VerifyNotComplete' +import AdditionalIdentificationRequiredScreen from './non-photo/AdditionalIdentificationRequiredScreen' +import DualIdentificationRequiredScreen from './non-photo/DualIdentificationRequiredScreen' +import EvidenceCaptureScreen from './non-photo/EvidenceCaptureScreen' +import EvidenceIDCollectionScreen from './non-photo/EvidenceIDCollectionScreen' +import EvidenceTypeListScreen from './non-photo/EvidenceTypeListScreen' +import IDPhotoInformationScreen from './non-photo/IDPhotoInformationScreen' import InformationRequiredScreen from './send-video/InformationRequiredScreen' -import PhotoInstructionsScreen from './send-video/PhotoInstructionsScreen' -import TakePhotoScreen from './send-video/TakePhotoScreen' -import PhotoReviewScreen from './send-video/PhotoReviewScreen' +import PendingReviewScreen from './send-video/PendingReviewScreen' +import SuccessfullySentScreen from './send-video/SuccessfullySentScreen' import TakeVideoScreen from './send-video/TakeVideoScreen' import VideoInstructionsScreen from './send-video/VideoInstructionsScreen' import VideoReviewScreen from './send-video/VideoReviewScreen' import VideoTooLongScreen from './send-video/VideoTooLongScreen' -import PendingReviewScreen from './send-video/PendingReviewScreen' -import SuccessfullySentScreen from './send-video/SuccessfullySentScreen' -import AdditionalIdentificationRequiredScreen from './non-photo/AdditionalIdentificationRequiredScreen' -import IDPhotoInformationScreen from './non-photo/IDPhotoInformationScreen' -import EvidenceTypeListScreen from './non-photo/EvidenceTypeListScreen' -import EvidenceCaptureScreen from './non-photo/EvidenceCaptureScreen' -import EvidenceIDCollectionScreen from './non-photo/EvidenceIDCollectionScreen' -import EnterEmailScreen from './email/EnterEmailScreen' -import EmailConfirmationScreen from './email/EmailConfirmationScreen' -import createHelpHeaderButton from '@/bcsc-theme/components/HelpHeaderButton' -import { ResidentialAddressScreen } from './ResidentialAddressScreen' -import { HelpCentreUrl } from '@/constants' -import WebViewScreen from '../webview/WebViewScreen' -import { createWebviewHeaderBackButton } from '@/bcsc-theme/components/WebViewBackButton' -import DualIdentificationRequiredScreen from './non-photo/DualIdentificationRequiredScreen' const VerifyIdentityStack = () => { const Stack = createStackNavigator() @@ -157,7 +162,6 @@ const VerifyIdentityStack = () => { headerRight: createHelpHeaderButton({ helpCentreUrl: HelpCentreUrl.HOME }), }} /> - { headerLeft: createWebviewHeaderBackButton(navigation), })} /> - + + + + + ) diff --git a/app/src/bcsc-theme/features/verify/live-call/BeforeYouCallScreen.tsx b/app/src/bcsc-theme/features/verify/live-call/BeforeYouCallScreen.tsx new file mode 100644 index 000000000..f39125f98 --- /dev/null +++ b/app/src/bcsc-theme/features/verify/live-call/BeforeYouCallScreen.tsx @@ -0,0 +1,102 @@ +import { BCSCScreens, BCSCVerifyIdentityStackParams } from '@bcsc-theme/types/navigators' +import { Button, ButtonType, testIdWithKey, ThemedText, useTheme } from '@bifold/core' +import { useNetInfo } from '@react-native-community/netinfo' +import { RouteProp } from '@react-navigation/native' +import { StackNavigationProp } from '@react-navigation/stack' +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { ScrollView, StyleSheet, View } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' + +type BeforeYouCallScreenProps = { + navigation: StackNavigationProp + route: RouteProp +} + +const BeforeYouCallScreen = ({ navigation, route }: BeforeYouCallScreenProps) => { + const { ColorPalette, Spacing } = useTheme() + const { type: networkType, isConnected } = useNetInfo() + const { t } = useTranslation() + const { formattedHours } = route.params || {} + + // Use the passed formatted hours or fallback to default + const hoursText = formattedHours || 'Monday to Friday\n7:30am - 5:00pm Pacific Time' + const isCellular = useMemo(() => networkType === 'cellular' && isConnected === true, [networkType, isConnected]) + + const styles = StyleSheet.create({ + pageContainer: { + flex: 1, + justifyContent: 'space-between', + backgroundColor: ColorPalette.brand.primaryBackground, + padding: Spacing.md, + }, + contentContainer: { + flexGrow: 1, + }, + controlsContainer: { + gap: Spacing.sm, + marginTop: Spacing.md, + }, + }) + + const onPressContinue = async () => { + navigation.navigate(BCSCScreens.TakePhoto, { + forLiveCall: true, + deviceSide: 'front', + cameraInstructions: '', + cameraLabel: '', + }) + } + + const onPressAssistance = () => { + // TODO (bm): webview or external link here presumeably + } + + return ( + + + + {t('Unified.VideoCall.BeforeYouCallTitle')} + + {t('Unified.VideoCall.WiFiRecommended')} + + {isCellular ? t('Unified.VideoCall.CellularNetworkWarning') : ''} + {t('Unified.VideoCall.StandardDataCharges')} + + + + {t('Unified.VideoCall.FindPrivatePlace')} + + {t('Unified.VideoCall.MakeSureOnlyYou')} + + + {t('Unified.VideoCall.HoursOfService')} + + {hoursText} + + {t('Unified.VideoCall.ContactCentrePrivacy')} + + {t(`Unified.VideoCall.PrivacyNotice`)} + {t(`Unified.VideoCall.PrivacyContactInfo`)} + + + + + {t('Unified.VideoCall.VerifyNotComplete.TroubleshootingTips')} + + + +