diff --git a/android/build.gradle b/android/build.gradle index b3ed68a727..e774a75a79 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -14,7 +14,7 @@ buildscript { jcenter() } dependencies { - classpath("com.android.tools.build:gradle:4.1.0") + classpath("com.android.tools.build:gradle:4.2.2") classpath 'com.google.gms:google-services:4.3.5' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' diff --git a/locales/en.json b/locales/en.json index b219e3de1f..fb3f1c556c 100644 --- a/locales/en.json +++ b/locales/en.json @@ -197,7 +197,7 @@ }, "rejected": { "title": "Notice", - "message": "You rejected {{sender}}'s {{vcLabel}}" + "message": "You discarded {{sender}}'s {{vcLabel}}" }, "disconnected": { "title": "Disconnected", @@ -240,7 +240,9 @@ "exchangingDeviceInfo": "Exchanging device info...", "exchangingDeviceInfoTimeout": "It's taking a while to exchange device info. You may have to reconnect.", "invalid": "Invalid QR Code", - "offline": "Please connect to the internet to scan QR codes using Online sharing mode" + "offline": "Please connect to the internet to scan QR codes using Online sharing mode", + "sent": "{{ vcLabel }} has been sent...", + "sentHint": "Waiting for receiver to save or discard your {{ vcLabel }}" } }, "SelectVcOverlay": { @@ -266,7 +268,7 @@ }, "rejected": { "title": "Notice", - "message": "Your {{vcLabel}} was rejected by {{receiver}}" + "message": "Your {{vcLabel}} was discarded by {{receiver}}" } }, "consentToPhotoVerification": "I give consent to have my photo taken for authentication" diff --git a/locales/fil.json b/locales/fil.json index 251283948e..e9ff77b861 100644 --- a/locales/fil.json +++ b/locales/fil.json @@ -197,7 +197,7 @@ }, "rejected": { "title": "Paunawa", - "message": "Tinanggihan mo ang {{vcLabel}} ni {{sender}}" + "message": "Iwinaksi ang {{vcLabel}} ni {{sender}}" }, "disconnected": { "title": "Nadiskonekta", @@ -240,7 +240,9 @@ "exchangingDeviceInfo": "Nagpapalitan ng impormasyon ng device...", "exchangingDeviceInfoTimeout": "Medyo nagtatagal ang paglabas ng impormasyon ng device. Bukas ba ang ibang device para sa mga koneksyon?", "invalid": "Di-wasto ang QR Code", - "offline": "Mangyaring kumonekta sa internet upang makapag-scan ng QR codes na gumagamit ng Online sharing mode" + "offline": "Mangyaring kumonekta sa internet upang makapag-scan ng QR codes na gumagamit ng Online sharing mode", + "sent": "Naibahagi na ang {{vcLabel}}...", + "sentHint": "Iniintay ang nakatanggap na itabi o iwaksi ang iyong {{vcLabel}}" } }, "SelectVcOverlay": { @@ -266,7 +268,7 @@ }, "rejected": { "title": "Pansinin", - "message": "Ang iyong {{vcLabel}} ay tinanggihan ng {{receiver}}" + "message": "Iwinaksi ni {{receiver}} ang iyong {{vcLabel}}" } }, "consentToPhotoVerification": "Nagbibigay ako ng pahintulot na kunin ang aking larawan para sa pagpapatunay" diff --git a/machines/biometrics.ts b/machines/biometrics.ts index b23a7b7a42..7177276714 100644 --- a/machines/biometrics.ts +++ b/machines/biometrics.ts @@ -1,6 +1,7 @@ import { createModel } from 'xstate/lib/model'; import * as LocalAuthentication from 'expo-local-authentication'; import { EventFrom, MetaObject, StateFrom } from 'xstate'; +import { Platform } from 'react-native'; // ----- CREATE MODEL --------------------------------------------------------- const model = createModel( @@ -97,6 +98,9 @@ export const biometricsMachine = model.createMachine( authenticating: { invoke: { src: () => async () => { + if (Platform.OS === 'android') { + await LocalAuthentication.cancelAuthenticate(); + } const res = await LocalAuthentication.authenticateAsync({ promptMessage: 'Biometric Authentication', @@ -112,6 +116,9 @@ export const biometricsMachine = model.createMachine( actions: ['setStatus'], }, }, + on: { + AUTHENTICATE: 'authenticating', + }, }, reauthenticating: { @@ -145,6 +152,7 @@ export const biometricsMachine = model.createMachine( SET_IS_AVAILABLE: { target: '#biometrics.available', }, + AUTHENTICATE: 'authenticating', }, }, @@ -186,6 +194,9 @@ export const biometricsMachine = model.createMachine( }, }, }, + on: { + AUTHENTICATE: 'authenticating', + }, }, }, }, diff --git a/machines/request.ts b/machines/request.ts index 35d53c7f4d..b8aa07ae44 100644 --- a/machines/request.ts +++ b/machines/request.ts @@ -304,6 +304,12 @@ export const requestMachine = }, reviewing: { + invoke: { + src: 'sendVcResponse', + data: { + status: 'RECEIVED', + }, + }, exit: 'disconnect', initial: 'idle', states: { @@ -399,7 +405,7 @@ export const requestMachine = }, }, accepted: { - entry: ['sendVcReceived', 'logReceived'], + entry: ['updateReceivedVcs', 'logReceived'], invoke: { src: 'sendVcResponse', data: { @@ -634,7 +640,7 @@ export const requestMachine = { to: (context) => context.serviceRefs.activityLog } ), - sendVcReceived: send( + updateReceivedVcs: send( (context) => { return VcEvents.VC_RECEIVED(VC_ITEM_STORE_KEY(context.incomingVc)); }, @@ -796,7 +802,7 @@ export const requestMachine = } }, - sendVcResponse: (context, _event, meta) => () => { + sendVcResponse: (context, _event, meta) => async () => { const event: SendVcResponseEvent = { type: 'send-vc:response', data: meta.data.status, @@ -807,7 +813,8 @@ export const requestMachine = // pass }); } else { - onlineSend(event); + await GoogleNearbyMessages.unpublish(); + await onlineSend(event); } }, diff --git a/machines/request.typegen.ts b/machines/request.typegen.ts index 7c1ebdca15..7869bdd552 100644 --- a/machines/request.typegen.ts +++ b/machines/request.typegen.ts @@ -35,7 +35,8 @@ export interface Typegen0 { sendDisconnect: 'done.invoke.request.cancelling:invocation[0]'; sendVcResponse: | 'done.invoke.request.reviewing.accepted:invocation[0]' - | 'done.invoke.request.reviewing.rejected:invocation[0]'; + | 'done.invoke.request.reviewing.rejected:invocation[0]' + | 'done.invoke.request.reviewing:invocation[0]'; verifyVp: 'done.invoke.request.reviewing.verifyingVp:invocation[0]'; }; 'missingImplementations': { @@ -86,7 +87,6 @@ export interface Typegen0 { | 'FACE_VALID' | 'done.invoke.request.reviewing.verifyingVp:invocation[0]'; requestReceiverInfo: 'CONNECTED'; - sendVcReceived: 'STORE_RESPONSE'; setIncomingVc: 'VC_RECEIVED'; setReceiveLogTypeDiscarded: 'CANCEL' | 'REJECT'; setReceiveLogTypeRegular: 'ACCEPT'; @@ -96,6 +96,7 @@ export interface Typegen0 { setSenderInfo: 'EXCHANGE_DONE'; storeVc: 'STORE_RESPONSE'; switchProtocol: 'SWITCH_PROTOCOL'; + updateReceivedVcs: 'STORE_RESPONSE'; }; 'eventsCausingDelays': { CANCEL_TIMEOUT: 'CANCEL'; @@ -122,7 +123,7 @@ export interface Typegen0 { receiveVc: 'EXCHANGE_DONE'; requestBluetooth: 'BLUETOOTH_DISABLED'; sendDisconnect: 'CANCEL'; - sendVcResponse: 'CANCEL' | 'REJECT' | 'STORE_RESPONSE'; + sendVcResponse: 'CANCEL' | 'REJECT' | 'STORE_RESPONSE' | 'VC_RECEIVED'; verifyVp: never; }; 'matchesStates': diff --git a/machines/scan.ts b/machines/scan.ts index 297bf84316..a9f8213953 100644 --- a/machines/scan.ts +++ b/machines/scan.ts @@ -61,6 +61,7 @@ const model = createModel( VERIFY_AND_ACCEPT_REQUEST: () => ({}), VC_ACCEPTED: () => ({}), VC_REJECTED: () => ({}), + VC_SENT: () => ({}), CANCEL: () => ({}), DISMISS: () => ({}), CONNECTED: () => ({}), @@ -232,12 +233,15 @@ export const scanMachine = initial: 'selectingVc', states: { selectingVc: { + invoke: { + src: 'monitorCancellation', + }, on: { UPDATE_REASON: { actions: 'setReason', }, DISCONNECT: { - target: '#scan.findingConnection', + target: '#scan.disconnected', }, SELECT_VC: { actions: 'setSelectedVc', @@ -256,6 +260,7 @@ export const scanMachine = actions: 'toggleShouldVerifyPresence', }, }, + exit: ['onlineUnsubscribe'], }, cancelling: { invoke: { @@ -291,16 +296,26 @@ export const scanMachine = }, }, }, + sent: { + description: + 'VC data has been shared and the receiver should now be viewing it', + on: { + VC_ACCEPTED: { + target: '#scan.reviewing.accepted', + }, + VC_REJECTED: { + target: '#scan.reviewing.rejected', + }, + }, + }, }, on: { DISCONNECT: { target: '#scan.findingConnection', }, - VC_ACCEPTED: { - target: 'accepted', - }, - VC_REJECTED: { - target: 'rejected', + VC_SENT: { + target: '#scan.reviewing.sendingVc.sent', + internal: true, }, }, }, @@ -610,6 +625,10 @@ export const scanMachine = shouldVerifyPresence: false, }), }), + + onlineUnsubscribe: () => { + GoogleNearbyMessages.unsubscribe(); + }, }, services: { @@ -649,6 +668,14 @@ export const scanMachine = } }, + monitorCancellation: (context) => async (callback) => { + if (context.sharingProtocol === 'ONLINE') { + await onlineSubscribe('disconnect', null, () => + callback({ type: 'DISCONNECT' }) + ); + } + }, + checkLocationStatus: () => (callback) => { checkLocation( () => callback(model.events.LOCATION_ENABLED()), @@ -737,10 +764,15 @@ export const scanMachine = }; const statusCallback = (status: SendVcStatus) => { + console.log('[scan] statusCallback', status); if (typeof status === 'number') return; - callback({ - type: status === 'ACCEPTED' ? 'VC_ACCEPTED' : 'VC_REJECTED', - }); + if (status === 'RECEIVED') { + callback({ type: 'VC_SENT' }); + } else { + callback({ + type: status === 'ACCEPTED' ? 'VC_ACCEPTED' : 'VC_REJECTED', + }); + } }; if (context.sharingProtocol === 'OFFLINE') { @@ -896,6 +928,10 @@ export function selectIsRejected(state: State) { return state.matches('reviewing.rejected'); } +export function selectIsSent(state: State) { + return state.matches('reviewing.sendingVc.sent'); +} + export function selectIsInvalid(state: State) { return state.matches('invalid'); } @@ -928,6 +964,10 @@ export function selectIsOffline(state: State) { return state.matches('offline'); } +export function selectIsDisconnected(state: State) { + return state.matches('disconnected'); +} + async function sendVc( vc: VC, callback: (status: SendVcStatus) => void, @@ -968,7 +1008,9 @@ async function sendVc( }, }); } else if (typeof status === 'string') { - GoogleNearbyMessages.unsubscribe(); + if (status === 'ACCEPTED' || status === 'REJECTED') { + GoogleNearbyMessages.unsubscribe(); + } callback(status); } }, diff --git a/machines/scan.typegen.ts b/machines/scan.typegen.ts index 7f0381a54b..bc6373a9cd 100644 --- a/machines/scan.typegen.ts +++ b/machines/scan.typegen.ts @@ -8,6 +8,10 @@ export interface Typegen0 { data: unknown; __tip: 'See the XState TS docs to learn how to strongly type this.'; }; + 'error.platform.scan.reviewing.creatingVp:invocation[0]': { + type: 'error.platform.scan.reviewing.creatingVp:invocation[0]'; + data: unknown; + }; 'xstate.after(CANCEL_TIMEOUT)#scan.reviewing.cancelling': { type: 'xstate.after(CANCEL_TIMEOUT)#scan.reviewing.cancelling'; }; @@ -33,6 +37,7 @@ export interface Typegen0 { createVp: 'done.invoke.scan.reviewing.creatingVp:invocation[0]'; discoverDevice: 'done.invoke.scan.connecting:invocation[0]'; exchangeDeviceInfo: 'done.invoke.scan.exchangingDeviceInfo:invocation[0]'; + monitorCancellation: 'done.invoke.scan.reviewing.selectingVc:invocation[0]'; monitorConnection: 'done.invoke.scan:invocation[0]'; sendDisconnect: 'done.invoke.scan.reviewing.cancelling:invocation[0]'; sendVc: 'done.invoke.scan.reviewing.sendingVc:invocation[0]'; @@ -73,6 +78,14 @@ export interface Typegen0 { | 'xstate.stop'; logFailedVerification: 'FACE_INVALID'; logShared: 'VC_ACCEPTED'; + onlineUnsubscribe: + | 'ACCEPT_REQUEST' + | 'CANCEL' + | 'DISCONNECT' + | 'SCREEN_BLUR' + | 'SCREEN_FOCUS' + | 'VERIFY_AND_ACCEPT_REQUEST' + | 'xstate.stop'; openSettings: 'LOCATION_REQUEST'; registerLoggers: | 'DISCONNECT' @@ -110,6 +123,7 @@ export interface Typegen0 { SHARING_TIMEOUT: | 'ACCEPT_REQUEST' | 'FACE_VALID' + | 'VC_SENT' | 'done.invoke.scan.reviewing.creatingVp:invocation[0]'; }; 'eventsCausingGuards': { @@ -125,11 +139,17 @@ export interface Typegen0 { exchangeDeviceInfo: | 'CONNECTED' | 'xstate.after(CONNECTION_TIMEOUT)#scan.exchangingDeviceInfo'; + monitorCancellation: + | 'CANCEL' + | 'DISMISS' + | 'EXCHANGE_DONE' + | 'error.platform.scan.reviewing.creatingVp:invocation[0]'; monitorConnection: 'xstate.init'; sendDisconnect: 'CANCEL'; sendVc: | 'ACCEPT_REQUEST' | 'FACE_VALID' + | 'VC_SENT' | 'done.invoke.scan.reviewing.creatingVp:invocation[0]'; }; 'matchesStates': @@ -163,6 +183,7 @@ export interface Typegen0 { | 'reviewing.selectingVc' | 'reviewing.sendingVc' | 'reviewing.sendingVc.inProgress' + | 'reviewing.sendingVc.sent' | 'reviewing.sendingVc.timeout' | 'reviewing.verifyingIdentity' | { @@ -184,7 +205,7 @@ export interface Typegen0 { | 'selectingVc' | 'sendingVc' | 'verifyingIdentity' - | { sendingVc?: 'inProgress' | 'timeout' }; + | { sendingVc?: 'inProgress' | 'sent' | 'timeout' }; }; 'tags': never; } diff --git a/machines/vc.typegen.ts b/machines/vc.typegen.ts index e5b7210f2e..7c9759a5d2 100644 --- a/machines/vc.typegen.ts +++ b/machines/vc.typegen.ts @@ -8,9 +8,9 @@ export interface Typegen0 { 'invokeSrcNameMap': {}; 'missingImplementations': { actions: never; - services: never; - guards: never; delays: never; + guards: never; + services: never; }; 'eventsCausingActions': { getReceivedVcsResponse: 'GET_RECEIVED_VCS'; @@ -24,11 +24,11 @@ export interface Typegen0 { setMyVcs: 'STORE_RESPONSE'; setReceivedVcs: 'STORE_RESPONSE'; }; - 'eventsCausingServices': {}; + 'eventsCausingDelays': {}; 'eventsCausingGuards': { hasExistingReceivedVc: 'VC_RECEIVED'; }; - 'eventsCausingDelays': {}; + 'eventsCausingServices': {}; 'matchesStates': | 'init' | 'init.myVcs' diff --git a/screens/Request/RequestScreen.strings.json b/screens/Request/RequestScreen.strings.json index f0a448cded..8ea8570510 100644 --- a/screens/Request/RequestScreen.strings.json +++ b/screens/Request/RequestScreen.strings.json @@ -10,7 +10,7 @@ }, "rejected": { "title": "Notice", - "message": "You rejected {{sender}}'s {{vcLabel}}" + "message": "You discarded {{sender}}'s {{vcLabel}}" }, "disconnected": { "title": "Disconnected", diff --git a/screens/Scan/ScanLayout.tsx b/screens/Scan/ScanLayout.tsx index d2cc808634..6cec676c01 100644 --- a/screens/Scan/ScanLayout.tsx +++ b/screens/Scan/ScanLayout.tsx @@ -10,6 +10,7 @@ import { useScanLayout } from './ScanLayoutController'; import { LanguageSelector } from '../../components/LanguageSelector'; import { ScanScreen } from './ScanScreen'; import { I18nManager, Platform } from 'react-native'; +import { Message } from '../../components/Message'; const ScanStack = createNativeStackNavigator(); @@ -68,6 +69,14 @@ export const ScanLayout: React.FC = () => { progress={!controller.isInvalid} onBackdropPress={controller.DISMISS_INVALID} /> + + {controller.isDisconnected && ( + + )} ); }; diff --git a/screens/Scan/ScanLayoutController.ts b/screens/Scan/ScanLayoutController.ts index 63c30c4fda..728aed4dde 100644 --- a/screens/Scan/ScanLayoutController.ts +++ b/screens/Scan/ScanLayoutController.ts @@ -16,6 +16,8 @@ import { selectIsReviewing, selectIsScanning, selectIsOffline, + selectIsSent, + selectIsDisconnected, } from '../../machines/scan'; import { selectVcLabel } from '../../machines/settings'; import { MainBottomTabParamList } from '../../routes/main'; @@ -30,6 +32,8 @@ type ScanLayoutNavigation = NavigationProp< ScanStackParamList & MainBottomTabParamList >; +// TODO: refactor +// eslint-disable-next-line sonarjs/cognitive-complexity export function useScanLayout() { const { t } = useTranslation('ScanScreen'); const { appService } = useContext(GlobalContext); @@ -65,6 +69,9 @@ export function useScanLayout() { selectIsExchangingDeviceInfoTimeout ); const isOffline = useSelector(scanService, selectIsOffline); + const isSent = useSelector(scanService, selectIsSent); + + const vcLabel = useSelector(settingsService, selectVcLabel); const onCancel = () => scanService.send(ScanEvents.CANCEL()); let statusOverlay: Pick< @@ -91,6 +98,11 @@ export function useScanLayout() { hint: t('status.exchangingDeviceInfoTimeout'), onCancel, }; + } else if (isSent) { + statusOverlay = { + message: t('status.sent', { vcLabel: vcLabel.singular }), + hint: t('status.sentHint', { vcLabel: vcLabel.singular }), + }; } else if (isInvalid) { statusOverlay = { message: t('status.invalid'), @@ -130,12 +142,14 @@ export function useScanLayout() { }, [isDone, isReviewing, isScanning]); return { - vcLabel: useSelector(settingsService, selectVcLabel), + vcLabel, isInvalid, isDone, + isDisconnected: useSelector(scanService, selectIsDisconnected), statusOverlay, + DISMISS: () => scanService.send(ScanEvents.DISMISS()), DISMISS_INVALID: () => isInvalid ? scanService.send(ScanEvents.DISMISS()) : null, }; diff --git a/screens/Scan/ScanScreen.strings.json b/screens/Scan/ScanScreen.strings.json index 083d38c5f8..67044c05d7 100644 --- a/screens/Scan/ScanScreen.strings.json +++ b/screens/Scan/ScanScreen.strings.json @@ -18,6 +18,8 @@ "exchangingDeviceInfo": "Exchanging device info...", "exchangingDeviceInfoTimeout": "It's taking a while to exchange device info. You may have to reconnect.", "invalid": "Invalid QR Code", - "offline": "Please connect to the internet to scan QR codes using Online sharing mode" + "offline": "Please connect to the internet to scan QR codes using Online sharing mode", + "sent": "{{ vcLabel }} has been sent...", + "sentHint": "Waiting for receiver to save or discard your {{ vcLabel }}" } } \ No newline at end of file diff --git a/screens/Scan/SendVcScreen.strings.json b/screens/Scan/SendVcScreen.strings.json index cbd0955285..a8adf7afb8 100644 --- a/screens/Scan/SendVcScreen.strings.json +++ b/screens/Scan/SendVcScreen.strings.json @@ -15,7 +15,7 @@ }, "rejected": { "title": "Notice", - "message": "Your {{vcLabel}} was rejected by {{receiver}}" + "message": "Your {{vcLabel}} was discarded by {{receiver}}" } }, "consentToPhotoVerification": "I give consent to have my photo taken for authentication" diff --git a/shared/smartshare.ts b/shared/smartshare.ts index 73efe7c2c8..9924c220f4 100644 --- a/shared/smartshare.ts +++ b/shared/smartshare.ts @@ -23,6 +23,7 @@ export function onlineSubscribe( } const response = SmartshareEvent.fromString(foundMessage); if (response.type === 'disconnect') { + GoogleNearbyMessages.unsubscribe(); disconectCallback(response.data); } else if (response.type === eventType) { !config?.keepAlive && GoogleNearbyMessages.unsubscribe(); @@ -34,7 +35,12 @@ export function onlineSubscribe( console.log('\n[request] MESSAGE_LOST', lostMessage.slice(0, 100)); } } - ); + ).catch((error: Error) => { + if (error.message.includes('existing callback is already subscribed')) { + console.log('Existing callback found. Unsubscribing then retrying...'); + return onlineSubscribe(eventType, callback, disconectCallback, config); + } + }); } export function onlineSend(event: SmartshareEvents) { @@ -113,7 +119,7 @@ export interface SendVcEvent { }; } -export type SendVcStatus = 'ACCEPTED' | 'REJECTED'; +export type SendVcStatus = 'ACCEPTED' | 'REJECTED' | 'RECEIVED'; export interface SendVcResponseEvent { type: 'send-vc:response'; data: SendVcStatus | number;