diff --git a/assets/Secure-Sharing.png b/assets/Secure-Sharing.png new file mode 100644 index 00000000..f635f8da Binary files /dev/null and b/assets/Secure-Sharing.png differ diff --git a/assets/Secure-Sharing2.png b/assets/Secure-Sharing2.png new file mode 100644 index 00000000..ae2d449d Binary files /dev/null and b/assets/Secure-Sharing2.png differ diff --git a/assets/inji_small_logo.png b/assets/inji_small_logo.png new file mode 100644 index 00000000..f7009186 Binary files /dev/null and b/assets/inji_small_logo.png differ diff --git a/assets/intro-scanner.png b/assets/intro-scanner.png new file mode 100644 index 00000000..fd8876e3 Binary files /dev/null and b/assets/intro-scanner.png differ diff --git a/assets/intro-wallet-binding.png b/assets/intro-wallet-binding.png new file mode 100644 index 00000000..fa59b5de Binary files /dev/null and b/assets/intro-wallet-binding.png differ diff --git a/components/KebabPopUp.tsx b/components/KebabPopUp.tsx new file mode 100644 index 00000000..9124b8ab --- /dev/null +++ b/components/KebabPopUp.tsx @@ -0,0 +1,65 @@ +import React, { useRef, useState } from 'react'; +import { t } from 'i18next'; +import { BottomSheet, Icon, ListItem } from 'react-native-elements'; +import { Theme } from '../components/ui/styleUtils'; +import { Centered, Column, Row, Text } from '../components/ui'; +import { WalletBinding } from '../screens/Home/MyVcs/WalletBinding'; + +export const KebabPopUpMenu: React.FC = (props) => { + return ( + + + + + {t('More Options')} + + + + + + + + + {t('Unpin Card')} + + + + + + + + + ); +}; + +interface KebabPopUpMenuProps { + isVisible: boolean; + close: () => void; +} diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index daad1abb..4d4ca1ec 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -44,6 +44,8 @@ export const Button: React.FC = (props) => { color={ type === 'solid' || type === 'addId' || type === 'radius' ? Theme.Colors.whiteText + : type === 'plain' + ? Theme.Colors.plainText : Theme.Colors.AddIdBtnTxt }> {props.title} diff --git a/components/ui/themes/DefaultTheme.ts b/components/ui/themes/DefaultTheme.ts index 66e06a61..f9d995b6 100644 --- a/components/ui/themes/DefaultTheme.ts +++ b/components/ui/themes/DefaultTheme.ts @@ -18,7 +18,8 @@ const Colors = { Warning: '#f0ad4e', LightOrange: '#fce7e3', GrayText: '#6F6F6F', - GrayText: '#6F6F6F', + dorColor: '#CBCBCB', + plainText: '#FFD6A7', }; export type ElevationLevel = 0 | 1 | 2 | 3 | 4 | 5; @@ -69,6 +70,8 @@ export const DefaultTheme = { ProfileIconBg: Colors.LightOrange, GrayText: Colors.GrayText, gradientBtn: ['#F59B4B', '#E86E04'], + dotColor: Colors.dorColor, + plainText: Colors.plainText, }, Styles: StyleSheet.create({ title: { @@ -695,6 +698,14 @@ export const DefaultTheme = { top: 40, zIndex: 1, }, + bottomContainer: { + padding: 20, + borderTopLeftRadius: 30, + borderTopRightRadius: 30, + marginTop: -185, + paddingBottom: 100, + height: 220, + }, }), claimsContainer: StyleSheet.create({ container: { @@ -710,7 +721,10 @@ export const DefaultTheme = { WarningLogo: require('../../../assets/warningLogo.png'), OtpLogo: require('../../../assets/otp-mobile-logo.png'), SuccessLogo: require('../../../assets/success-logo.png'), - + sharingIntro: require('../../../assets/Secure-Sharing.png'), + walletIntro: require('../../../assets/intro-wallet-binding.png'), + IntroScanner: require('../../../assets/intro-scanner.png'), + injiSmallLogo: require('../../../assets/inji_small_logo.png'), elevation(level: ElevationLevel): ViewStyle { // https://ethercreative.github.io/react-native-shadow-generator/ diff --git a/locales/en.json b/locales/en.json index d5f1d2cc..b580b9bd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -156,13 +156,15 @@ "generateVcDescription": "Tap on \"Add {{vcLabel}}\" below to download your {{vcLabel}}" }, "OnboardingOverlay": { - "stepOneTitle": "Welcome!", - "stepOneText": "Keep your digital credential with you at all times. To get started, add {{vcLabel}} to your profile.", - "stepTwoTitle": "{{vcLabel}} management", - "stepTwoText": "Once generated, {{vcLabel}} are safely stored on your mobile and can be renamed or shared at any time.", - "stepThreeTitle": "Easy sharing", - "stepThreeText": "Share and receive {{vcLabel}} switfly using your phone camera to scan QR codes.", - "stepThreeButton": "Get started and add {{vcLabel}}" + "stepOneTitle": "Secure Sharing!", + "stepOneText": "Share and receive {{vcLabel}} switfly using your phone camera to scan QR codes", + "stepTwoTitle": "Trusted Digital Wallet", + "stepTwoText": " Keep your digital credential with you at all times", + "stepThreeTitle": "Quick Access", + "stepThreeText": "Once generated, {{vcLabel}} are safely stored on your mobile and can be renamed or shared at any time.", + "stepThreeButton": "Get Started", + "skip": "Skip", + "next": "Next" }, "ReceivedVcsTab": { "noReceivedVcsTitle": "No {{vcLabel}} available yet", diff --git a/machines/WalletBinding.typegen.ts b/machines/WalletBinding.typegen.ts new file mode 100644 index 00000000..044a17b3 --- /dev/null +++ b/machines/WalletBinding.typegen.ts @@ -0,0 +1,94 @@ +// This file was automatically generated. Edits will be overwritten + +export interface Typegen0 { + '@@xstate/typegen': true; + 'internalEvents': { + 'done.invoke.walletBinding.addKeyPair:invocation[0]': { + type: 'done.invoke.walletBinding.addKeyPair:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.walletBinding.addingWalletBindingId:invocation[0]': { + type: 'done.invoke.walletBinding.addingWalletBindingId:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.walletBinding.requestingBindingOtp:invocation[0]': { + type: 'done.invoke.walletBinding.requestingBindingOtp:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'done.invoke.walletBinding.updatingPrivateKey:invocation[0]': { + type: 'done.invoke.walletBinding.updatingPrivateKey:invocation[0]'; + data: unknown; + __tip: 'See the XState TS docs to learn how to strongly type this.'; + }; + 'error.platform.walletBinding.addKeyPair:invocation[0]': { + type: 'error.platform.walletBinding.addKeyPair:invocation[0]'; + data: unknown; + }; + 'error.platform.walletBinding.addingWalletBindingId:invocation[0]': { + type: 'error.platform.walletBinding.addingWalletBindingId:invocation[0]'; + data: unknown; + }; + 'error.platform.walletBinding.requestingBindingOtp:invocation[0]': { + type: 'error.platform.walletBinding.requestingBindingOtp:invocation[0]'; + data: unknown; + }; + 'error.platform.walletBinding.updatingPrivateKey:invocation[0]': { + type: 'error.platform.walletBinding.updatingPrivateKey:invocation[0]'; + data: unknown; + }; + 'xstate.init': { type: 'xstate.init' }; + }; + 'invokeSrcNameMap': { + addWalletBindnigId: 'done.invoke.walletBinding.addingWalletBindingId:invocation[0]'; + generateKeyPair: 'done.invoke.walletBinding.addKeyPair:invocation[0]'; + requestBindingOtp: 'done.invoke.walletBinding.requestingBindingOtp:invocation[0]'; + updatePrivateKey: 'done.invoke.walletBinding.updatingPrivateKey:invocation[0]'; + }; + 'missingImplementations': { + actions: 'clearTransactionId'; + delays: never; + guards: never; + services: never; + }; + 'eventsCausingActions': { + clearOtp: + | 'DISMISS' + | 'done.invoke.walletBinding.requestingBindingOtp:invocation[0]'; + clearTransactionId: 'DISMISS'; + setOtp: 'INPUT_OTP'; + setPrivateKey: 'done.invoke.walletBinding.addKeyPair:invocation[0]'; + setPublicKey: 'done.invoke.walletBinding.addKeyPair:invocation[0]'; + setWalletBindingError: + | 'error.platform.walletBinding.addKeyPair:invocation[0]' + | 'error.platform.walletBinding.addingWalletBindingId:invocation[0]' + | 'error.platform.walletBinding.requestingBindingOtp:invocation[0]' + | 'error.platform.walletBinding.updatingPrivateKey:invocation[0]'; + setWalletBindingErrorEmpty: + | 'CANCEL' + | 'done.invoke.walletBinding.updatingPrivateKey:invocation[0]'; + setWalletBindingId: 'done.invoke.walletBinding.addingWalletBindingId:invocation[0]'; + storeContext: 'done.invoke.walletBinding.updatingPrivateKey:invocation[0]'; + updatePrivateKey: 'done.invoke.walletBinding.updatingPrivateKey:invocation[0]'; + updateVc: 'done.invoke.walletBinding.updatingPrivateKey:invocation[0]'; + }; + 'eventsCausingDelays': {}; + 'eventsCausingGuards': {}; + 'eventsCausingServices': { + addWalletBindnigId: 'done.invoke.walletBinding.addKeyPair:invocation[0]'; + generateKeyPair: 'INPUT_OTP'; + requestBindingOtp: 'CONFIRM'; + updatePrivateKey: 'done.invoke.walletBinding.addingWalletBindingId:invocation[0]'; + }; + 'matchesStates': + | 'acceptingBindingOtp' + | 'addKeyPair' + | 'addingWalletBindingId' + | 'requestingBindingOtp' + | 'showBindingWarning' + | 'showingWalletBindingError' + | 'updatingPrivateKey'; + 'tags': never; +} diff --git a/machines/auth.ts b/machines/auth.ts index 9bdc0ad2..16b650a2 100644 --- a/machines/auth.ts +++ b/machines/auth.ts @@ -21,6 +21,7 @@ const model = createModel( LOGIN: () => ({}), STORE_RESPONSE: (response?: unknown) => ({ response }), SELECT: () => ({}), + NEXT: () => ({}), }, } ); @@ -71,6 +72,13 @@ export const authMachine = model.createMachine( languagesetup: { on: { SELECT: { + target: 'introSlider', + }, + }, + }, + introSlider: { + on: { + NEXT: { target: 'settingUp', }, }, @@ -200,3 +208,6 @@ export function selectSettingUp(state: State) { export function selectLanguagesetup(state: State) { return state.matches('languagesetup'); } +export function selectIntroSlider(state: State) { + return state.matches('introSlider'); +} diff --git a/machines/auth.typegen.ts b/machines/auth.typegen.ts index 465f890b..2f5d254e 100644 --- a/machines/auth.typegen.ts +++ b/machines/auth.typegen.ts @@ -46,6 +46,7 @@ export interface Typegen0 { | 'authorized' | 'checkingAuth' | 'init' + | 'introSlider' | 'languagesetup' | 'savingDefaults' | 'settingUp' diff --git a/machines/vcItem.typegen.ts b/machines/vcItem.typegen.ts index 3e96e46c..8d81f39a 100644 --- a/machines/vcItem.typegen.ts +++ b/machines/vcItem.typegen.ts @@ -75,6 +75,10 @@ export interface Typegen0 { type: 'error.platform.vc-item.addingWalletBindingId:invocation[0]'; data: unknown; }; + 'error.platform.vc-item.checkingServerData.verifyingDownloadLimitExpiry:invocation[0]': { + type: 'error.platform.vc-item.checkingServerData.verifyingDownloadLimitExpiry:invocation[0]'; + data: unknown; + }; 'error.platform.vc-item.requestingBindingOtp:invocation[0]': { type: 'error.platform.vc-item.requestingBindingOtp:invocation[0]'; data: unknown; @@ -149,6 +153,7 @@ export interface Typegen0 { | 'CREDENTIAL_DOWNLOADED' | 'GET_VC_RESPONSE' | 'STORE_RESPONSE'; + setDownloadInterval: 'done.invoke.vc-item.checkingServerData.verifyingDownloadLimitExpiry:invocation[0]'; setLock: 'done.invoke.vc-item.requestingLock:invocation[0]'; setMaxDownloadCount: 'done.invoke.vc-item.checkingServerData.verifyingDownloadLimitExpiry:invocation[0]'; setOtp: 'INPUT_OTP'; @@ -196,7 +201,9 @@ export interface Typegen0 { 'eventsCausingServices': { addWalletBindnigId: 'done.invoke.vc-item.addKeyPair:invocation[0]'; checkDownloadExpiryLimit: 'STORE_RESPONSE'; - checkStatus: 'done.invoke.vc-item.checkingServerData.verifyingDownloadLimitExpiry:invocation[0]'; + checkStatus: + | 'done.invoke.vc-item.checkingServerData.verifyingDownloadLimitExpiry:invocation[0]' + | 'error.platform.vc-item.checkingServerData.verifyingDownloadLimitExpiry:invocation[0]'; downloadCredential: 'DOWNLOAD_READY'; generateKeyPair: 'INPUT_OTP'; requestBindingOtp: 'CONFIRM'; diff --git a/machines/walletBinding.ts b/machines/walletBinding.ts new file mode 100644 index 00000000..7b018f31 --- /dev/null +++ b/machines/walletBinding.ts @@ -0,0 +1,345 @@ +import { TextInput } from 'react-native'; +import { assign, ErrorPlatformEvent, StateFrom, send, EventFrom } from 'xstate'; +import { log } from 'xstate/lib/actions'; + +import i18n from '../i18n'; +import { AppServices } from '../shared/GlobalContext'; +import { ActivityLogEvents } from './activityLog'; +import { StoreEvents } from './store'; +import { createModel } from 'xstate/lib/model'; +import { request } from '../shared/request'; +import { VcIdType } from '../types/vc'; +import { MY_VCS_STORE_KEY, VC_ITEM_STORE_KEY } from '../shared/constants'; +import { + generateKeys, + WalletBindingResponse, +} from '../shared/cryptoutil/cryptoUtil'; +import { KeyPair } from 'react-native-rsa-native'; +import { + getBindingCertificateConstant, + savePrivateKey, +} from '../shared/keystore/SecureKeystore'; + +const model = createModel( + { + serviceRefs: {} as AppServices, + idType: 'VID' as VcIdType, + id: '', + idError: '', + otp: '', + otpError: '', + transactionId: '', + requestId: '', + VIDs: [] as string[], + bindingTransactionId: '', + walletBindingResponse: null as WalletBindingResponse, + walletBindingError: '', + publicKey: '', + privateKey: '', + }, + { + events: { + INPUT_OTP: (otp: string) => ({ otp }), + VALIDATE_INPUT: () => ({}), + READY: (idInputRef: TextInput) => ({ idInputRef }), + DISMISS: () => ({}), + SELECT_ID_TYPE: (idType: VcIdType) => ({ idType }), + REVOKE_VCS: (vcKeys: string[]) => ({ vcKeys }), + STORE_RESPONSE: (response: string[]) => ({ response }), + ERROR: (data: Error) => ({ data }), + SUCCESS: () => ({}), + CONFIRM: () => ({}), + CANCEL: () => ({}), + }, + } +); + +export const walletBindingMachine = + /** @xstate-layout N4IgpgJg5mDOIC5QCUwDcD2BrMA1AlhLAHSEA2YAxMgKK4DyA0jQPq4DCAyoqAA4ax8AF3wYAdjxAAPRACYAHAFZiARhUB2WQDZFABgCcO-boDMWgDQgAnohWyALLOK6X9k-J3zZirfYC+fpaomDgERKRiaACGZISUAJIAcgAKAKoAKiz06cmS-IIi4pIyCOq6TuomjvYqWlpe3iaWNgiKik4u5fK69vr2uipmAUHo2HiEJPiRMXEAIvGcALIL3Egg+cKiEmslZRVVDrX1so3Nclr6zi61pvLyKoY9wyDBY2EkUQDGn2C8ImJQXDxWaTCAUah0JisDirPgCTZFHaIEyydTEe6yEyaQzqXrtM4IeQmXRXcrqDz9dQPEzPV6hCbEL4-P5TQHAkgAJzAAEcAK5wf5QehCXiUCDiMAREKSunjcJM36CoEg4hcvkC1nC3gIKaYT5RQpiADaugAunl4Ybioh8dZbHtiHoevpFESlIp9DTAi9RvT5d9FazlZyefzYIKtZQwByORgOcReGQDQAzOMAW2IsvejIDLIBwdVoY1AK1OsiGH1hpN5rWGytSIQJix6LsWNkOLxsgJKLRnVMlXJdxq-m9WYZCrzbJBlHmSxWFoKW2tjdRLcx2K0uJdXbtCGMpN03Qe-X03VktN9co+uYjIviYl4vKECRSGSyOQXCO2oBKKkPaNdRQqXsRRBi0EwXQJep5EdFxURdTczB0C9pWzCdb14e9H2fWdlk4WF1ktJcGwUZQ1E0HQDCMUwLF3NQ+lJVFN3kXFvBQt4GTVMNBVlMUJSlMZM0vbMuOLKBZTLPUDS2atP3rH9bH-R0lGA0CzAgxQCRqS4nQ0F0-10SoRxGVDOKLcNWV46NY3jRMU3TITTPCUSLIBCTdQraTxFk2siMRBTSh3FpBhdWCeiAqkiSA4yfSckMQiDT4ZwWPCCLrYiAq8AlBgeYg6jKLF1DaB5ygCb0xAwCA4EkMdwnIMA5Iy6RbEMPKNGJSo2nuD17Gyh5lDqC59HUTwVBqL0TI4urpliCBiAwEVGv85rWipZx5GGxRiV8GjNN3BQALg8bMS0citHYv1JhmwhiAAIy+HAxAgJbvxWto0T6doTlkFwtBOFRuzaMK1G6TdqWGi6rylGZnt8xdlpKLaTGIT7vp+3Q-tkAH9pUZRdNOux9KxGLauvZklXZUgwQauGv2XH7LhMOwai+zcfvkXrd3sHQwsPal7C8WpFEhtCbyDSmXIwl7l00btud5twLj-Op5BF8cxfzdlpYbAXspYhW9iG3w1f9cnNTvB8n21gKBbROpHEAhn20UTngp+lRiCx7nN1uAxMRNkN1Vc8TL2tlaiTRQZtBMCKBe+gkXZJJ1tB6ECBf0FQA8LBL80+MPf3bGC7lRDHT1TptuzuMLDhRMwzoD-O5Fd2wYOuVF-raXx7HUMq-CAA */ + model.createMachine( + { + predictableActionArguments: true, + preserveActionOrder: true, + tsTypes: {} as import('./WalletBinding.typegen').Typegen0, + schema: { + context: model.initialContext, + events: {} as EventFrom, + }, + id: 'walletBinding', + initial: 'showBindingWarning', + states: { + showBindingWarning: { + on: { + CONFIRM: { + target: 'requestingBindingOtp', + }, + CANCEL: { + target: 'showBindingWarning', + }, + }, + }, + requestingBindingOtp: { + invoke: { + src: 'requestBindingOtp', + onDone: [ + { + target: 'acceptingBindingOtp', + }, + ], + onError: [ + { + actions: 'setWalletBindingError', + target: 'showingWalletBindingError', + }, + ], + }, + }, + showingWalletBindingError: { + on: { + CANCEL: { + target: 'showBindingWarning', + actions: 'setWalletBindingErrorEmpty', + }, + }, + }, + acceptingBindingOtp: { + entry: ['clearOtp'], + on: { + INPUT_OTP: { + target: 'addKeyPair', + actions: ['setOtp'], + }, + DISMISS: { + target: '', + actions: ['clearOtp', 'clearTransactionId'], + }, + }, + }, + addKeyPair: { + invoke: { + src: 'generateKeyPair', + onDone: { + target: 'addingWalletBindingId', + actions: ['setPublicKey', 'setPrivateKey'], + }, + onError: [ + { + actions: 'setWalletBindingError', + target: 'showingWalletBindingError', + }, + ], + }, + }, + addingWalletBindingId: { + invoke: { + src: 'addWalletBindnigId', + onDone: [ + { + target: 'updatingPrivateKey', + actions: ['setWalletBindingId'], + }, + ], + onError: [ + { + actions: 'setWalletBindingError', + target: 'showingWalletBindingError', + }, + ], + }, + }, + updatingPrivateKey: { + invoke: { + src: 'updatePrivateKey', + onDone: { + target: '', + actions: [ + 'storeContext', + 'updatePrivateKey', + 'updateVc', + 'setWalletBindingErrorEmpty', + ], + }, + onError: { + actions: 'setWalletBindingError', + target: 'showingWalletBindingError', + }, + }, + }, + }, + }, + { + actions: { + setWalletBindingError: assign({ + walletBindingError: (context, event) => (event.data as Error).message, + }), + + setWalletBindingErrorEmpty: assign({ + walletBindingError: () => '', + }), + + setPublicKey: assign({ + publicKey: (context, event) => (event.data as KeyPair).public, + }), + + setPrivateKey: assign({ + privateKey: (context, event) => (event.data as KeyPair).private, + }), + + updatePrivateKey: assign({ + privateKey: () => '', + }), + + setWalletBindingId: assign({ + walletBindingResponse: (context, event) => + event.data as WalletBindingResponse, + }), + + setOtp: model.assign({ + otp: (_context, event) => event.otp, + }), + + clearOtp: assign({ otp: '' }), + + storeContext: send( + (context) => { + const { serviceRefs, ...data } = context; + return StoreEvents.SET(VC_ITEM_STORE_KEY(context), data); + }, + { + to: (context) => context.serviceRefs.store, + } + ), + + updateVc: send( + (context) => { + const { serviceRefs, ...vc } = context; + return { type: 'VC_DOWNLOADED', vc }; + }, + { + to: (context) => context.serviceRefs.vc, + } + ), + }, + + services: { + addWalletBindnigId: async (context) => { + const response = await request( + 'POST', + '/residentmobileapp/wallet-binding', + { + requestTime: String(new Date().toISOString()), + request: { + authFactorType: 'WLA', + format: 'jwt', + individualId: context.id, + transactionId: context.bindingTransactionId, + publicKey: context.publicKey, + challengeList: [ + { + authFactorType: 'OTP', + challenge: context.otp, + format: 'alpha-numeric', + }, + ], + }, + } + ); + const certificate = response.response.certificate; + await savePrivateKey( + getBindingCertificateConstant(context.id), + certificate + ); + + const walletResponse: WalletBindingResponse = { + walletBindingId: response.response.encryptedWalletBindingId, + keyId: response.response.keyId, + thumbprint: response.response.thumbprint, + expireDateTime: response.response.expireDateTime, + }; + return walletResponse; + }, + + updatePrivateKey: async (context) => { + const hasSetPrivateKey: boolean = await savePrivateKey( + context.walletBindingResponse.walletBindingId, + context.privateKey + ); + if (!hasSetPrivateKey) { + throw new Error('Could not store private key in keystore.'); + } + return ''; + }, + + generateKeyPair: async (context) => { + let keyPair: KeyPair = await generateKeys(); + return keyPair; + }, + + requestBindingOtp: async (context) => { + console.log('requesting otp'); + const response = await request( + 'POST', + '/residentmobileapp/binding-otp', + { + requestTime: String(new Date().toISOString()), + request: { + individualId: context.id, + otpChannels: ['EMAIL', 'PHONE'], + }, + } + ); + if (response.response == null) { + throw new Error('Could not process request'); + } + }, + }, + + guards: {}, + } + ); + +export function createWalletBindingMachine(serviceRefs: AppServices) { + return walletBindingMachine.withContext({ + ...walletBindingMachine.context, + serviceRefs, + }); +} + +type State = StateFrom; + +export const WalletBindingEvents = model.events; + +export function selectIdType(state: State) { + return state.context.idType; +} + +export function selectIdError(state: State) { + return state.context.idError; +} + +export function selectOtpError(state: State) { + return state.context.otpError; +} +export function selectWalletBindingError(state: State) { + return state.context.walletBindingError; +} +export function selectIsBindingWarning(state: State) { + return state.matches('showBindingWarning'); +} + +export function selectIsAcceptingOtpInput(state: State) { + return state.matches('acceptingBindingOtp'); +} + +export function walletBindingInProgress(state: State) { + return state.matches('requestingBindingOtp') || + state.matches('addingWalletBindingId') || + state.matches('addKeyPair') || + state.matches('updatingPrivateKey') + ? true + : false; +} + +export function selectShowWalletBindingError(state: State) { + return state.matches('showingWalletBindingError'); +} diff --git a/routes/index.ts b/routes/index.ts index 6d80c6d3..d44f6d83 100644 --- a/routes/index.ts +++ b/routes/index.ts @@ -10,12 +10,20 @@ import { PasscodeScreen } from '../screens/PasscodeScreen'; import { MainLayout } from '../screens/MainLayout'; import { NotificationsScreen } from '../screens/NotificationsScreen'; import { SetupLanguageScreen } from '../screens/SetupLanguageScreen'; +import { IntroSlidersScreen } from '../screens/Home/IntroSlidersScreen'; export const baseRoutes: Screen[] = [ { name: 'Language', component: SetupLanguageScreen, }, + { + name: 'IntroSliders', + component: IntroSlidersScreen, + options: { + headerShown: false, + }, + }, { name: 'Welcome', component: WelcomeScreen, @@ -50,6 +58,7 @@ export const authRoutes: Screen[] = [ export type RootStackParamList = { Language: undefined; + IntroSliders: undefined; Welcome: undefined; Auth: undefined; Passcode: { diff --git a/screens/AppLayout.tsx b/screens/AppLayout.tsx index 621e38ef..cfdf5537 100644 --- a/screens/AppLayout.tsx +++ b/screens/AppLayout.tsx @@ -25,7 +25,7 @@ export const AppLayout: React.FC = () => { {baseRoutes.map((route) => ( diff --git a/screens/Home/IntroSlidersScreen.tsx b/screens/Home/IntroSlidersScreen.tsx new file mode 100644 index 00000000..14b2c497 --- /dev/null +++ b/screens/Home/IntroSlidersScreen.tsx @@ -0,0 +1,147 @@ +import React, { useRef, useContext } from 'react'; +import AppIntroSlider from 'react-native-app-intro-slider'; +import { + Dimensions, + Image, + StatusBar, + TouchableOpacity, + View, +} from 'react-native'; +import { Centered, Column, Row, Text, Button } from '../../components/ui'; +import { Theme } from '../../components/ui/styleUtils'; +import { useSelector } from '@xstate/react'; +import { GlobalContext } from '../../shared/GlobalContext'; +import { selectVcLabel } from '../../machines/settings'; +import { useTranslation } from 'react-i18next'; +import { RootRouteProps } from '../../routes'; +import { useWelcomeScreen } from '../WelcomeScreenController'; +import LinearGradient from 'react-native-linear-gradient'; + +export const IntroSlidersScreen: React.FC = (props) => { + const slider = useRef(); + + const { t } = useTranslation('OnboardingOverlay'); + const { appService } = useContext(GlobalContext); + const settingsService = appService.children.get('settings'); + const vcLabel = useSelector(settingsService, selectVcLabel); + const controller = useWelcomeScreen(props); + + const slides = [ + { + key: 'one', + title: t('stepOneTitle'), + text: t('stepOneText', { vcLabel: vcLabel.plural }), + image: Theme.sharingIntro, + }, + { + key: 'two', + title: t('stepTwoTitle', { vcLabel: vcLabel.singular }), + text: t('stepTwoText', { vcLabel: vcLabel.plural }), + image: Theme.walletIntro, + }, + { + key: 'three', + title: t('stepThreeTitle'), + text: t('stepThreeText', { vcLabel: vcLabel.plural }), + image: Theme.IntroScanner, + }, + ]; + + const renderItem = ({ item }) => { + return ( + + + + + + + + +