diff --git a/.eslintrc.js b/.eslintrc.js index 429047159..197abc71f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -40,7 +40,7 @@ module.exports = { 'prefer-const': ['error'], semi: ['error', 'never'], 'use-isnan': ['error'], - '@typescript-eslint/array-type': ['error', 'array-simple'], + '@typescript-eslint/array-type': ['error', { default: 'array-simple' }], '@typescript-eslint/ban-types': [ 'error', { @@ -54,11 +54,11 @@ module.exports = { ], 'react/jsx-uses-vars': ['warn'], // PascalCase for classes - '@typescript-eslint/class-name-casing': ['error'], + '@typescript-eslint/class-name-casing': ['off'], // don't prefix interface names with 'I' - '@typescript-eslint/interface-name-prefix': ['error', 'never'], + '@typescript-eslint/interface-name-prefix': ['off'], // don't conflict and JSX - '@typescript-eslint/no-angle-bracket-type-assertion': ['error'], + '@typescript-eslint/no-angle-bracket-type-assertion': ['off'], // lose out on typing benefits with any '@typescript-eslint/no-explicit-any': ['error'], '@typescript-eslint/no-empty-interface': ['off'], @@ -66,10 +66,15 @@ module.exports = { // namespaces and modules are outdated, use ES6 style '@typescript-eslint/no-namespace': ['error'], // use ES6-style imports instead - '@typescript-eslint/no-triple-slash-reference': ['error'], + '@typescript-eslint/no-triple-slash-reference': ['off'], '@typescript-eslint/no-var-requires': ['off'], '@typescript-eslint/no-use-before-define': ['off'], '@typescript-eslint/explicit-function-return-type': ['off'], - 'import/no-duplicates': ['error'], + 'import/no-duplicates': ['off'], }, + ignorePatterns: [ + 'package.json', + 'jest.config.js', + 'app.json', + ], } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39baa9152..e270b261b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: - node-version: [10.x] + node-version: [12.x] steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index f21343c4b..82b13b895 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ appcenter-secrets report.xml .bundle/ vendor/ +poeditor-key/ # App connect keys app-store-connect-auth diff --git a/.prettierrc.js b/.prettierrc.js index f1823d81a..b99274970 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,7 +1,6 @@ module.exports = { - endOfLine: 'lf', semi: false, singleQuote: true, tabWidth: 2, trailingComma: 'all', -}; +} diff --git a/App.tsx b/App.tsx index 08337a38c..039b46630 100644 --- a/App.tsx +++ b/App.tsx @@ -7,7 +7,7 @@ import { SafeAreaProvider } from 'react-native-safe-area-context' import { I18nextProvider } from 'react-i18next' import RootNavigation from '~/RootNavigation' -import { ErrorBoundary } from '~/errors/ErrorBoundary' +import ErrorBoundary from '~/errors/ErrorBoundary' import { AgentContextProvider } from '~/utils/sdk/context' import configureStore from './configureStore' import Overlays from '~/Overlays' @@ -28,8 +28,8 @@ const App = () => { return ( - - + + @@ -38,8 +38,8 @@ const App = () => { - - + + ) } diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..eeecbeeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +Copyright 2014-2021 Jolocom GmbH \ No newline at end of file diff --git a/__tests__/mocks/agent.ts b/__tests__/mocks/agent.ts new file mode 100644 index 000000000..1f7bf3a36 --- /dev/null +++ b/__tests__/mocks/agent.ts @@ -0,0 +1,5 @@ +export const mockedAgent = { + passwordStore: { + getPassword: jest.fn().mockResolvedValue(true), + }, +} diff --git a/__tests__/mocks/documents.ts b/__tests__/mocks/documents.ts new file mode 100644 index 000000000..ca16b0f67 --- /dev/null +++ b/__tests__/mocks/documents.ts @@ -0,0 +1,78 @@ +export const mockedDocuments = { + documents: [ + { + id: 'abscbajhfjdhfjdshfsdjhfa', + type: 'document', + claim: { + id: 'id1', + message: 'message1', + }, + metadata: { + name: 'Document 1', + }, + issuer: { + did: 'did:jun:example', + }, + }, + { + id: 'dsfjsjdfjhdfasjdhfasdhjfajsdhf', + type: 'document 2', + claim: { + id: 'id2', + message: 'message2', + }, + metadata: { + name: 'Document 2', + }, + issuer: { + did: 'did:jun:example', + publicProfile: { + name: 'Issuer name', + description: 'I am the issuer', + }, + }, + }, + ], + other: [ + { + id: 'adfdjfahdfahdfajsdhf dfye', + type: 'other', + claim: { + id: 'id3', + message: 'message3', + }, + metadata: { + name: 'Document 3', + }, + issuer: { + did: 'did:jun:example', + publicProfile: { + name: 'Issuer name', + description: 'I am the issuer', + }, + }, + }, + ], +} + +export const mockedFields = [ + { + id: 1, + type: 'document', + details: { + mandatoryFields: [ + { label: 'givenName', value: 'Test Given Name' }, + { label: 'Document Name', value: 'some doc' }, + ], + optionalFields: [{ label: 'c', value: 'd' }], + }, + }, + { + id: 2, + type: 'other', + details: { + mandatoryFields: [{ label: 'givenName', value: 'f' }], + optionalFields: [{ label: 'g', value: 'h' }], + }, + }, +] diff --git a/__tests__/mocks/libs/react-redux.ts b/__tests__/mocks/libs/react-redux.ts new file mode 100644 index 000000000..a4858d67e --- /dev/null +++ b/__tests__/mocks/libs/react-redux.ts @@ -0,0 +1,17 @@ +import * as rredux from 'react-redux' + +type MockedStore = Record | MockedStore + +export function mockSelectorReturn(mockedStore: MockedStore) { + // @ts-expect-error + rredux.useSelector.mockImplementation((callback: (state: any) => void) => + callback(mockedStore), + ) +} + +export const getMockedDispatch = () => { + const mockDispatchFn = jest.fn() + const useDispatchSpy = jest.spyOn(rredux, 'useDispatch') + useDispatchSpy.mockReturnValue(mockDispatchFn) + return mockDispatchFn +} diff --git a/__tests__/mocks/react-native-safe-area-context.js b/__tests__/mocks/react-native-safe-area-context.js deleted file mode 100644 index a01fa0fe5..000000000 --- a/__tests__/mocks/react-native-safe-area-context.js +++ /dev/null @@ -1,13 +0,0 @@ -const inset = { - top: 0, - right: 0, - bottom: 0, - left: 0, -} - -export const SafeAreaConsumer = ({ children }) => { - return children(inset) -} - -//FIXME: Any better way to do this? -test.skip('Workaround', () => 1) diff --git a/__tests__/mocks/store/attributes.ts b/__tests__/mocks/store/attributes.ts new file mode 100644 index 000000000..29ac6a075 --- /dev/null +++ b/__tests__/mocks/store/attributes.ts @@ -0,0 +1,33 @@ +export const mockedNoAttributes = { + account: { did: 'did-1' }, + toasts: { active: null }, + attrs: { + all: {}, + }, +} + +export const mockedAttributes = { + attrs: { + all: { + ProofOfEmailCredential: [ + { id: 'claimId', value: { givenName: 'Karl', familyName: 'Muller' } }, + ], + }, + }, +} + +export const getMockedEmailAttribute = ( + attrId1: string, + attrId2: string, + email1: string, + email2: string, +) => ({ + attrs: { + all: { + ProofOfEmailCredential: [ + { id: attrId1, value: { email: email1 } }, + { id: attrId2, value: { email: email2 } }, + ], + }, + }, +}) diff --git a/__tests__/suits/Documents/Cards.test.tsx b/__tests__/suits/Documents/Cards.test.tsx deleted file mode 100644 index edb393917..000000000 --- a/__tests__/suits/Documents/Cards.test.tsx +++ /dev/null @@ -1,153 +0,0 @@ -import { fireEvent } from '@testing-library/react-native' -import React from 'react' -import DocumentCard from '~/components/Card/DocumentCard' -import OtherCard from '~/components/Card/OtherCard' -import { renderWithSafeArea } from '../../utils/renderWithSafeArea' - -const HIGHLIGHT = 'ABC123' -const IMAGE = 'data:abc' -const FIELDS = [ - { - id: 1, - type: 'document', - details: { - mandatoryFields: [ - { label: 'givenName', value: 'Test Given Name' }, - { label: 'Document Name', value: 'some doc' }, - ], - optionalFields: [{ label: 'c', value: 'd' }], - }, - }, - { - id: 2, - type: 'other', - details: { - mandatoryFields: [{ label: 'givenName', value: 'f' }], - optionalFields: [{ label: 'g', value: 'h' }], - }, - }, -] -const CLAIMS = [ - { - label: 'claim1', - value: 'Claim 1', - }, - { label: 'claim1', value: 'Claim 1' }, -] - -const testIds = { - photo: 'card-photo', - highlight: 'card-highlight', - logo: 'card-logo', - more: 'card-action-more', - popupMenu: 'popup-menu', -} - -const mockedNavigate = jest.fn() - -jest.mock('../../../src/components/Tabs/context', () => ({ - useTabs: jest.fn().mockReturnValue({ - activeTab: { id: 'document', value: 'Documents' }, - setActiveTab: jest.fn(), - }), -})) - -jest.mock('../../../src/hooks/toasts', () => ({ - useToasts: jest.fn().mockImplementation(() => ({ - scheduleWarning: jest.fn(), - })), -})) - -jest.mock('@react-navigation/native', () => ({ - ...jest.requireActual('@react-navigation/native'), - useNavigation: () => ({ - navigate: mockedNavigate, - }), -})) - -jest.mock('../../../src/hooks/credentials', () => ({ - useDeleteCredential: () => jest.fn(), -})) - -const [mandatoryFields] = FIELDS.map((f) => f.details.mandatoryFields) - -const [optionalFields] = FIELDS.map((f) => f.details.optionalFields) - -describe('Document card is displaying passed props', () => { - // TODO: fix me - xtest('documents with image and highlight ', () => { - const { getByText, getByTestId, queryByText } = renderWithSafeArea( - , - ) - - console.log(getByText('Test Given Name')) - expect(getByText(HIGHLIGHT)).toBeDefined() - expect(getByTestId(testIds.highlight)).toBeDefined() - expect(getByTestId(testIds.photo)).toBeDefined() - - expect(getByTestId(testIds.more)).toBeDefined() - // renders type documents - expect(getByText('b')).toBeDefined() - // doesn't render type other - expect(queryByText('f')).toBe(null) - expect(queryByText('Type of the document')).toBe(null) - }) - - test('without image and highlight', () => { - const { queryByTestId } = renderWithSafeArea( - , - ) - - expect(queryByTestId(testIds.photo)).toBe(null) - expect(queryByTestId(testIds.highlight)).toBe(null) - }) - - test('can perform actions on a card', () => { - const { getByTestId, getByText, debug } = renderWithSafeArea( - , - ) - - const dots = getByTestId(testIds.more) - fireEvent.press(dots) - - const popupMenu = getByTestId(testIds.popupMenu) - - expect(popupMenu.props.visible).toBe(true) - - const deleteBtn = getByText('Delete') - - fireEvent.press(deleteBtn) - expect(mockedNavigate).toHaveBeenCalledTimes(1) - }) -}) - -describe('Other card is displaying passed props', () => { - test('no logo', () => { - const { queryByTestId, getByText } = renderWithSafeArea( - , - ) - - expect(queryByTestId(testIds.logo)).toBeNull() - expect(getByText('Type of the document')).toBeDefined() - }) -}) diff --git a/__tests__/suits/Form/CredentialForm.test.tsx b/__tests__/suits/Form/CredentialForm.test.tsx index 9932b2f9a..bf0309367 100644 --- a/__tests__/suits/Form/CredentialForm.test.tsx +++ b/__tests__/suits/Form/CredentialForm.test.tsx @@ -3,12 +3,12 @@ import CredentialForm from '~/screens/Modals/Forms/CredentialForm' import { useRoute } from '@react-navigation/native' import { renderWithSafeArea } from '../../utils/renderWithSafeArea' import { AttributeTypes, ClaimKeys } from '~/types/credentials' -import { mockSelectorReturn } from '../../utils/selector' -import useTranslation from '~/hooks/useTranslation' +import { mockSelectorReturn } from '../../mocks/libs/react-redux' import { fireEvent, waitFor } from '@testing-library/react-native' import { editAttr, updateAttrs } from '~/modules/attributes/actions' -import { getMockedDispatch } from '../../utils/dispatch' -import { strings } from '~/translations' +import { getMockedDispatch } from '../../mocks/libs/react-redux' +import { mockedAgent } from '../../mocks/agent' +import { mockedNoAttributes } from '../../mocks/store/attributes' const ATTRIBUTE_ID = 'claim:email:id' const ATTRIBUTE_ID_UPDATED = 'claim:email:id-1' @@ -32,13 +32,6 @@ const mockedStore = { }, }, } -const mockedStoreNoAttributes = { - account: { did: 'did-1' }, - toasts: { active: null }, - attrs: { - all: {}, - }, -} const mockedIssueCredentialFn = jest.fn() const mockDeleteVCFn = jest.fn() @@ -46,14 +39,11 @@ const mockDeleteVCFn = jest.fn() jest.mock('@react-navigation/native') jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper') -jest.mock('../../../src/hooks/useTranslation') jest.mock('../../../src/hooks/sdk', () => ({ useAgent: () => ({ - passwordStore: { - getPassword: jest.fn().mockResolvedValue(true), - }, + passwordStore: mockedAgent.passwordStore, credentials: { - issue: mockedIssueCredentialFn, + create: mockedIssueCredentialFn, delete: mockDeleteVCFn, }, }), @@ -82,16 +72,6 @@ describe('Form in mode', () => { let mockDispatchFn: jest.Mock beforeAll(() => { - // @ts-expect-error - useTranslation.mockReturnValue({ - t: jest.fn().mockReturnValue('Something'), // TODO: This will return something for title and description - }) - // @ts-expect-error - useTranslation.mockImplementation(() => ({ - t: (text: string) => { - return text - }, - })) mockDispatchFn = getMockedDispatch() }) @@ -122,13 +102,9 @@ describe('Form in mode', () => { const queries = renderCredentialForm() expect( - queries.getAllByText(strings.EDIT_YOUR_ATTRIBUTE).length, - ).toBeDefined() - expect( - queries.getByText( - strings.ONCE_YOU_CLICK_DONE_IT_WILL_BE_DISPLAYED_IN_THE_PERSONAL_INFO_SECTION, - ), + queries.getAllByText(/CredentialForm.editHeader/).length, ).toBeDefined() + expect(queries.getByText(/CredentialForm.subheader/)).toBeDefined() // ASSERT ASYNC SUBMIT HANDLING await waitFor(() => { @@ -158,19 +134,15 @@ describe('Form in mode', () => { type: ATTRIBUTE_TYPE, }, }) - mockSelectorReturn(mockedStoreNoAttributes) + mockSelectorReturn(mockedNoAttributes) // RENDER const queries = renderCredentialForm() expect( - queries.getAllByText(strings.ADD_YOUR_ATTRIBUTE).length, - ).toBeDefined() - expect( - queries.getByText( - strings.ONCE_YOU_CLICK_DONE_IT_WILL_BE_DISPLAYED_IN_THE_PERSONAL_INFO_SECTION, - ), + queries.getAllByText(/CredentialForm.addHeader/).length, ).toBeDefined() + expect(queries.getByText(/CredentialForm.subheader/)).toBeDefined() // ASSERT ASYNC SUBMIT HANDLING await waitFor(() => { diff --git a/__tests__/suits/History/Record.test.tsx b/__tests__/suits/History/Record.test.tsx index b554b8328..5ce117310 100644 --- a/__tests__/suits/History/Record.test.tsx +++ b/__tests__/suits/History/Record.test.tsx @@ -3,6 +3,11 @@ import { waitFor, act, fireEvent } from '@testing-library/react-native' import { renderWithSafeArea } from '../../utils/renderWithSafeArea' import History from '~/screens/LoggedIn/History' +jest.mock('../../../src/hooks/interactions/listeners', () => ({ + useInteractionUpdate: jest.fn(), + useInteractionCreate: jest.fn(), +})) + jest.mock('../../../src/hooks/history', () => ({ useHistory: () => ({ getInteractions: jest.fn().mockResolvedValueOnce([ @@ -10,7 +15,7 @@ jest.mock('../../../src/hooks/history', () => ({ { id: 'test-auth', section: 'Yesterday', type: 'Authentication' }, { id: 'test-share', section: 'Yesterday', type: 'CredentialShare' }, ]), - getInteractionDetails: jest.fn().mockResolvedValueOnce({ + assembleInteractionDetails: jest.fn().mockResolvedValueOnce({ issuer: { did: 'did: test' }, status: 'finished', steps: [ diff --git a/__tests__/suits/History/RecordAssembler.test.ts b/__tests__/suits/History/RecordAssembler.test.ts index 8e7dac942..8d6920b9e 100644 --- a/__tests__/suits/History/RecordAssembler.test.ts +++ b/__tests__/suits/History/RecordAssembler.test.ts @@ -1,19 +1,22 @@ import { RecordAssembler } from '~/middleware/records/recordAssembler' import { FlowType } from '@jolocom/sdk' -import { recordConfig } from '~/config/records' +// import { recordConfig } from '~/config/records' import { IRecordStatus } from '~/types/records' import { InteractionType } from 'jolocom-lib/js/interactionTokens/types' import truncateDid from '~/utils/truncateDid' import { capitalizeWord } from '~/utils/stringUtils' +import { recordConfig } from '~/hooks/history/utils' + +const mockedConfig = recordConfig const addDays = (days: number) => { - var result = new Date() + const result = new Date() result.setDate(result.getDate() + days) return result.getTime() } const subDays = (days: number) => { - var result = new Date() + const result = new Date() result.setDate(result.getDate() - days) return result.getTime() } @@ -25,7 +28,7 @@ const buildSummary = (state: T) => ({ const genericArgs = { lastMessageDate: Date.now(), expirationDate: addDays(1), - config: recordConfig, + config: mockedConfig, } const genericAuthArgs = { @@ -48,7 +51,7 @@ const genericAuthzArgs = { const genericOfferArgs = { ...genericArgs, ...buildSummary({ - offerSummary: [{ type: 'test-type' }], + offerSummary: [{ type: 'test-type', credential: { name: 'test-name' } }], issued: [{ type: 'test-type', name: 'test-name' }], }), flowType: FlowType.CredentialOffer, @@ -183,20 +186,19 @@ describe('Record Assembler', () => { const steps = assembler.getRecordDetails().steps expect(steps[steps.length - 1]).toStrictEqual({ - title: recordConfig.Authentication?.steps.unfinished[1], - description: 'Expired', + title: mockedConfig.flows.Authentication?.steps.unfinished[1], + description: 'History.expiredState', }) }) it('should return the correct authentication steps', () => { const assembler = new RecordAssembler(genericAuthArgs) - const expectedSteps = recordConfig.Authentication?.steps.finished.map( - (title) => ({ + const expectedSteps = + mockedConfig.flows.Authentication?.steps.finished.map((title) => ({ title, description: truncateDid(genericAuthArgs.summary.initiator.did), - }), - ) + })) expect(assembler.getRecordDetails().steps).toEqual(expectedSteps) }) @@ -204,12 +206,11 @@ describe('Record Assembler', () => { it('should return the correct authorization steps', () => { const assembler = new RecordAssembler(genericAuthzArgs) - const expectedSteps = recordConfig.Authorization?.steps.finished.map( - (title) => ({ + const expectedSteps = + mockedConfig.flows.Authorization?.steps.finished.map((title) => ({ title, description: capitalizeWord(genericAuthzArgs.summary.state.action), - }), - ) + })) expect(assembler.getRecordDetails().steps).toEqual(expectedSteps) }) @@ -217,19 +218,18 @@ describe('Record Assembler', () => { it('should return the correct offer steps', () => { const assembler = new RecordAssembler(genericOfferArgs) - const expectedSteps = recordConfig.CredentialOffer?.steps.finished.map( - (title, i) => ({ + const expectedSteps = + mockedConfig.flows.CredentialOffer?.steps.finished.map((title, i) => ({ title, description: i !== 2 ? genericOfferArgs.summary.state.offerSummary - .map((s) => s.type) + .map((s) => s.credential.name) .join(', ') : genericOfferArgs.summary.state.issued .map((s) => s.name) .join(', '), - }), - ) + })) expect(assembler.getRecordDetails().steps).toEqual(expectedSteps) }) @@ -237,15 +237,14 @@ describe('Record Assembler', () => { it('should return the correct share steps', () => { const assembler = new RecordAssembler(genericRequestArgs) - const expectedSteps = recordConfig.CredentialShare?.steps.finished.map( - (title) => ({ + const expectedSteps = + mockedConfig.flows.CredentialShare?.steps.finished.map((title) => ({ title, description: genericRequestArgs.summary.state.providedCredentials[0].suppliedCredentials .map((c) => c.name) .join(', '), - }), - ) + })) expect(assembler.getRecordDetails().steps).toEqual(expectedSteps) }) diff --git a/__tests__/suits/History/RecordItem.test.tsx b/__tests__/suits/History/RecordItem.test.tsx index 447fc8001..c317dfde2 100644 --- a/__tests__/suits/History/RecordItem.test.tsx +++ b/__tests__/suits/History/RecordItem.test.tsx @@ -31,17 +31,27 @@ const testAuthDetails = { } describe('Record Item', () => { - it('should render the placeholder when the details are unavailable', () => { + it('should render the placeholder when the details are unavailable', async () => { mockHistoryDetails() - const props = { id: 'test', onDropdown: jest.fn(), isFocused: false } + const props = { + id: 'test', + onDropdown: jest.fn(), + isFocused: false, + lastUpdated: Date.now().toString(), + } const { toJSON } = renderWithSafeArea() - expect(toJSON()).toMatchSnapshot() + await waitFor(() => expect(toJSON()).toMatchSnapshot()) }) it('should render a generic Record Item', async () => { mockHistoryDetails(testAuthDetails) - const props = { id: 'test', onDropdown: jest.fn(), isFocused: false } + const props = { + id: 'test', + onDropdown: jest.fn(), + isFocused: false, + lastUpdated: Date.now().toString(), + } const { toJSON } = await waitFor(() => renderWithSafeArea(), ) @@ -51,7 +61,12 @@ describe('Record Item', () => { it('should render the dropdown with the FINISHED state', async () => { mockHistoryDetails(testAuthDetails) - const props = { id: 'test', onDropdown: jest.fn(), isFocused: true } + const props = { + id: 'test', + onDropdown: jest.fn(), + isFocused: true, + lastUpdated: Date.now().toString(), + } const { toJSON } = await waitFor(() => renderWithSafeArea(), ) @@ -60,7 +75,12 @@ describe('Record Item', () => { }) it('should render the dropdown with the PENDING state', async () => { mockHistoryDetails({ ...testAuthDetails, status: IRecordStatus.pending }) - const props = { id: 'test', onDropdown: jest.fn(), isFocused: true } + const props = { + id: 'test', + onDropdown: jest.fn(), + isFocused: true, + lastUpdated: Date.now().toString(), + } const { toJSON } = await waitFor(() => renderWithSafeArea(), ) @@ -69,7 +89,12 @@ describe('Record Item', () => { }), it('should render the dropdown with the EXPIRED state', async () => { mockHistoryDetails({ ...testAuthDetails, status: IRecordStatus.expired }) - const props = { id: 'test', onDropdown: jest.fn(), isFocused: true } + const props = { + id: 'test', + onDropdown: jest.fn(), + isFocused: true, + lastUpdated: Date.now().toString(), + } const { toJSON } = await waitFor(() => renderWithSafeArea(), ) diff --git a/__tests__/suits/History/__snapshots__/RecordItem.test.tsx.snap b/__tests__/suits/History/__snapshots__/RecordItem.test.tsx.snap index 07c795f7f..f1dfa7c7e 100644 --- a/__tests__/suits/History/__snapshots__/RecordItem.test.tsx.snap +++ b/__tests__/suits/History/__snapshots__/RecordItem.test.tsx.snap @@ -30,7 +30,7 @@ exports[`Record Item should render a generic Record Item 1`] = ` style={ Object { "alignItems": "center", - "backgroundColor": "#000", + "backgroundColor": "rgba(6,1,7, 0.55)", "borderRadius": 15, "flexDirection": "row", "height": 80, @@ -160,12 +160,13 @@ exports[`Record Item should render a generic Record Item 1`] = ` "alignItems": "flex-end", "flexDirection": "row", "justifyContent": "space-between", - "marginBottom": 8, + "marginBottom": 0, "width": "100%", } } > @@ -201,6 +205,7 @@ exports[`Record Item should render a generic Record Item 1`] = ` }, Object { "alignSelf": "center", + "marginLeft": 8, "marginRight": 16, }, ] @@ -266,7 +271,7 @@ exports[`Record Item should render the dropdown with the EXPIRED state 1`] = ` style={ Object { "alignItems": "center", - "backgroundColor": "#000", + "backgroundColor": "rgba(6,1,7, 0.55)", "borderRadius": 15, "flexDirection": "row", "height": 80, @@ -396,12 +401,13 @@ exports[`Record Item should render the dropdown with the EXPIRED state 1`] = ` "alignItems": "flex-end", "flexDirection": "row", "justifyContent": "space-between", - "marginBottom": 8, + "marginBottom": 0, "width": "100%", } } > @@ -437,6 +446,7 @@ exports[`Record Item should render the dropdown with the EXPIRED state 1`] = ` }, Object { "alignSelf": "center", + "marginLeft": 8, "marginRight": 16, }, ] @@ -691,6 +701,7 @@ exports[`Record Item should render the dropdown with the EXPIRED state 1`] = ` style={ Object { "flex": 0.75, + "justifyContent": "center", "paddingLeft": 4, } } @@ -777,7 +788,7 @@ exports[`Record Item should render the dropdown with the FINISHED state 1`] = ` style={ Object { "alignItems": "center", - "backgroundColor": "#000", + "backgroundColor": "rgba(6,1,7, 0.55)", "borderRadius": 15, "flexDirection": "row", "height": 80, @@ -907,12 +918,13 @@ exports[`Record Item should render the dropdown with the FINISHED state 1`] = ` "alignItems": "flex-end", "flexDirection": "row", "justifyContent": "space-between", - "marginBottom": 8, + "marginBottom": 0, "width": "100%", } } > @@ -948,6 +963,7 @@ exports[`Record Item should render the dropdown with the FINISHED state 1`] = ` }, Object { "alignSelf": "center", + "marginLeft": 8, "marginRight": 16, }, ] @@ -1202,6 +1218,7 @@ exports[`Record Item should render the dropdown with the FINISHED state 1`] = ` style={ Object { "flex": 0.75, + "justifyContent": "center", "paddingLeft": 4, } } @@ -1288,7 +1305,7 @@ exports[`Record Item should render the dropdown with the PENDING state 1`] = ` style={ Object { "alignItems": "center", - "backgroundColor": "#000", + "backgroundColor": "rgba(6,1,7, 0.55)", "borderRadius": 15, "flexDirection": "row", "height": 80, @@ -1418,12 +1435,13 @@ exports[`Record Item should render the dropdown with the PENDING state 1`] = ` "alignItems": "flex-end", "flexDirection": "row", "justifyContent": "space-between", - "marginBottom": 8, + "marginBottom": 0, "width": "100%", } } > @@ -1459,6 +1480,7 @@ exports[`Record Item should render the dropdown with the PENDING state 1`] = ` }, Object { "alignSelf": "center", + "marginLeft": 8, "marginRight": 16, }, ] @@ -1713,6 +1735,7 @@ exports[`Record Item should render the dropdown with the PENDING state 1`] = ` style={ Object { "flex": 0.75, + "justifyContent": "center", "paddingLeft": 4, } } @@ -1799,7 +1822,7 @@ exports[`Record Item should render the placeholder when the details are unavaila style={ Object { "alignItems": "center", - "backgroundColor": "#000", + "backgroundColor": "rgba(6,1,7, 0.55)", "borderRadius": 15, "flexDirection": "row", "height": 80, @@ -1929,12 +1952,13 @@ exports[`Record Item should render the placeholder when the details are unavaila "alignItems": "flex-end", "flexDirection": "row", "justifyContent": "space-between", - "marginBottom": 8, + "marginBottom": 0, "width": "100%", } } > - ███████ - + /> - ██ - + /> - █████ + Unknown diff --git a/__tests__/suits/History/useHistory.test.ts b/__tests__/suits/History/useHistory.test.ts index e4686a9a4..406bb80c7 100644 --- a/__tests__/suits/History/useHistory.test.ts +++ b/__tests__/suits/History/useHistory.test.ts @@ -44,7 +44,7 @@ describe('useHistory Hook', () => { const today = grouped.find((r) => r.id === 'today-record') const yesterday = grouped.find((r) => r.id === 'yesterday-record') - expect(today?.section).toBe('Today') - expect(yesterday?.section).toBe('Yesterday') + expect(today?.section).toBe('Dates.today') + expect(yesterday?.section).toBe('Dates.yesterday') }) }) diff --git a/__tests__/suits/Identity/Home.test.tsx b/__tests__/suits/Identity/Home.test.tsx index 4bc99ec4f..3466924b1 100644 --- a/__tests__/suits/Identity/Home.test.tsx +++ b/__tests__/suits/Identity/Home.test.tsx @@ -1,33 +1,17 @@ import React from 'react' import Identity from '~/screens/LoggedIn/Identity' +import { + mockedAttributes, + mockedNoAttributes, +} from '../../mocks/store/attributes' import { renderWithSafeArea } from '../../utils/renderWithSafeArea' -import { mockSelectorReturn } from '../../utils/selector' - -const mockedNoAttributes = { - attrs: { - all: {}, - }, -} - -const mockedAttributes = { - attrs: { - all: { - ProofOfEmailCredential: [ - { id: 'claimId', value: { givenName: 'Karl', familyName: 'Muller' } }, - ], - }, - }, -} - -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn(), -})) +import { mockSelectorReturn } from '../../mocks/libs/react-redux' /* Mocking these components as we are not interested to render it fully at this stage */ -jest.mock('../../../src/screens/LoggedIn/Identity/IdentityIntro', () => () => - null, +jest.mock( + '../../../src/screens/LoggedIn/Identity/IdentityIntro', + () => () => null, ) jest.mock( '../../../src/screens/LoggedIn/Identity/IdentityCredentials', diff --git a/__tests__/suits/Identity/Intro.test.tsx b/__tests__/suits/Identity/Intro.test.tsx index 59355b571..fb6d0c19f 100644 --- a/__tests__/suits/Identity/Intro.test.tsx +++ b/__tests__/suits/Identity/Intro.test.tsx @@ -6,27 +6,26 @@ import { getQueriesForElement, waitFor, } from '@testing-library/react-native' -import { strings } from '~/translations' -import { getMockedDispatch } from '../../utils/dispatch' +import { getMockedDispatch } from '../../mocks/libs/react-redux' import { updateAttrs } from '~/modules/attributes/actions' import { AttributeTypes } from '~/types/credentials' import { ReactTestInstance } from 'react-test-renderer' +import { mockedAgent } from '../../mocks/agent' const ATTRIBUTE_ID = 'claim:id-1' const GIVEN_NAME = 'Karl' const FAMILY_NAME = 'Muller' const mockedIssueCredentialFn = jest.fn() +// eslint-disable-next-line const noop = () => {} jest.mock('react-native/Libraries/Animated/src/NativeAnimatedHelper') jest.mock('../../../src/hooks/sdk', () => ({ useAgent: () => ({ - passwordStore: { - getPassword: jest.fn().mockResolvedValue(true), - }, + passwordStore: mockedAgent.passwordStore, credentials: { - issue: mockedIssueCredentialFn, + create: mockedIssueCredentialFn, }, }), })) @@ -52,9 +51,9 @@ describe('Intro displays', () => { singleCredentialButton, ) - expect(getSingleCredText(strings.START_NOW)).toBeDefined() + expect(getSingleCredText(/Identity.widgetStartBtn/)).toBeDefined() - expect(getByText(strings.IT_IS_TIME_TO_CREATE)).toBeDefined() + expect(getByText(/Identity.widgetWelcome/)).toBeDefined() }) beforeEach(() => { @@ -79,7 +78,7 @@ describe('Intro displays', () => { const singleCredentialButton = getByTestId('single-credential-button') fireEvent.press(singleCredentialButton) - expect(getByText(strings.WHAT_IS_YOUR_NAME)).toBeDefined() + expect(getByText(/Identity.widgetNameHeader/)).toBeDefined() const submitBtnStep1 = getByTestId('button') fireEvent.press(submitBtnStep1) diff --git a/__tests__/suits/Identity/PrimitiveCredentials.test.tsx b/__tests__/suits/Identity/PrimitiveCredentials.test.tsx index 6aa1a7ca3..5fba645d4 100644 --- a/__tests__/suits/Identity/PrimitiveCredentials.test.tsx +++ b/__tests__/suits/Identity/PrimitiveCredentials.test.tsx @@ -5,38 +5,20 @@ import IdentityCredentials from '~/screens/LoggedIn/Identity/IdentityCredentials import { strings } from '~/translations' import { AttributeTypes } from '~/types/credentials' import { ScreenNames } from '~/types/screens' +import { + getMockedEmailAttribute, + mockedNoAttributes, +} from '../../mocks/store/attributes' import { renderWithSafeArea } from '../../utils/renderWithSafeArea' -import { mockSelectorReturn } from '../../utils/selector' +import { mockSelectorReturn } from '../../mocks/libs/react-redux' const ATTRIBUTE_ID_1 = 'claim:id-1' const ATTRIBUTE_ID_2 = 'claim:id-2' const EMAIL_VALUE_1 = 'dev-1@jolocom.com' const EMAIL_VALUE_2 = 'dev-2@jolocom.com' -const mockedStoreNoAttributes = { - attrs: { - all: {}, - }, -} - -const mockedStoreEmailAttribute = { - attrs: { - all: { - ProofOfEmailCredential: [ - { id: ATTRIBUTE_ID_1, value: { email: EMAIL_VALUE_1 } }, - { id: ATTRIBUTE_ID_2, value: { email: EMAIL_VALUE_2 } }, - ], - }, - }, -} - const mockedNavigate = jest.fn() -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn(), -})) - jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), useNavigation: () => ({ @@ -71,11 +53,11 @@ describe('Primitive credentials component displays', () => { }) test('placeholders if there are no credentials', () => { - mockSelectorReturn(mockedStoreNoAttributes) + mockSelectorReturn(mockedNoAttributes) const { getByText, getAllByTestId } = renderWithSafeArea( , ) - expect(getByText(strings.YOUR_INFO_IS_QUITE_EMPTY)).toBeDefined() + expect(getByText(/Identity.attributesMissingInfo/)).toBeDefined() const emptyFields = getAllByTestId('widget-field-empty') expect(emptyFields.length).toBe(4) @@ -91,7 +73,52 @@ describe('Primitive credentials component displays', () => { clickAndAssertNavigationCall(AttributeTypes.postalAddress) }) - test('credential values', () => { + test('fields are rendered', () => { + const mockedStoreEmailAttribute = getMockedEmailAttribute( + ATTRIBUTE_ID_1, + ATTRIBUTE_ID_2, + EMAIL_VALUE_1, + EMAIL_VALUE_2, + ) + mockSelectorReturn(mockedStoreEmailAttribute) + + const { getAllByTestId } = renderWithSafeArea() + + expect(getAllByTestId('widget-field-static')).toHaveLength(2) + }) + + test('adding on press create new', () => { + const mockedStoreEmailAttribute = getMockedEmailAttribute( + ATTRIBUTE_ID_1, + ATTRIBUTE_ID_2, + EMAIL_VALUE_1, + EMAIL_VALUE_2, + ) + mockSelectorReturn(mockedStoreEmailAttribute) + const { getByTestId } = renderWithSafeArea() + + const { getAllByTestId } = getQueriesForElement( + getByTestId('id-widget-with-values'), + ) + + // Part2: (without id) asserting if correct parameters where passed in navigation + const addNewOneButtonsEmail = getAllByTestId('widget-add-new') + + mockedNavigate.mockClear() + const clickAndAssertNavigationCallEmail = pressFieldAndAssertNavigation( + mockedNavigate, + addNewOneButtonsEmail, + ) + clickAndAssertNavigationCallEmail(AttributeTypes.emailAddress) + }) + + test('navigating on press', () => { + const mockedStoreEmailAttribute = getMockedEmailAttribute( + ATTRIBUTE_ID_1, + ATTRIBUTE_ID_2, + EMAIL_VALUE_1, + EMAIL_VALUE_2, + ) const { attrs: { all: { ProofOfEmailCredential }, @@ -100,22 +127,7 @@ describe('Primitive credentials component displays', () => { mockSelectorReturn(mockedStoreEmailAttribute) const { getAllByTestId } = renderWithSafeArea() - const widgets = getAllByTestId('widget') - expect(widgets.length).toBe(4) - - const { - getByText: getByTextEmailWidget, - getAllByTestId: getAllByTestIdEmailWidget, - } = getQueriesForElement(widgets[0]) - // asserting sorting and values are there - expect( - getByTextEmailWidget(ProofOfEmailCredential[0].value.email), - ).toBeDefined() - expect( - getByTextEmailWidget(ProofOfEmailCredential[1].value.email), - ).toBeDefined() - - const emailFields = getAllByTestIdEmailWidget('widget-field-static') + const emailFields = getAllByTestId('widget-field-static') // Part1: (with id) asserting if correct parameters where passed in navigation const clickAndAssertNavigationCall = pressFieldAndAssertNavigation( @@ -130,15 +142,5 @@ describe('Primitive credentials component displays', () => { AttributeTypes.emailAddress, ProofOfEmailCredential[1].id, ) - - // Part2: (without id) asserting if correct parameters where passed in navigation - const addNewOneButtonsEmail = getAllByTestIdEmailWidget('widget-add-new') - - mockedNavigate.mockClear() - const clickAndAssertNavigationCallEmail = pressFieldAndAssertNavigation( - mockedNavigate, - addNewOneButtonsEmail, - ) - clickAndAssertNavigationCallEmail(AttributeTypes.emailAddress) }) }) diff --git a/__tests__/suits/Interactions/Flows.test.tsx b/__tests__/suits/Interactions/Flows.test.tsx index 6d69225ae..3a8b45e86 100644 --- a/__tests__/suits/Interactions/Flows.test.tsx +++ b/__tests__/suits/Interactions/Flows.test.tsx @@ -1,8 +1,8 @@ import { act, renderHook } from '@testing-library/react-hooks' import * as interactionsHooks from '~/hooks/interactions/handlers' -import { mockSelectorReturn } from '../../utils/selector' -import { getMockedDispatch } from '../../utils/dispatch' +import { mockSelectorReturn } from '../../mocks/libs/react-redux' +import { getMockedDispatch } from '../../mocks/libs/react-redux' import { FlowType } from 'react-native-jolocom' import { setInteractionDetails } from '~/modules/interaction/actions' @@ -20,6 +20,9 @@ const mockProcessJWTRequestOffer = jest.fn() jest.mock('../../../src/hooks/sdk', () => ({ useAgent: () => ({ processJWT: mockProcessJWTRequestOffer, + idw: { + did: 'did:example', + }, }), })) @@ -34,14 +37,11 @@ jest.mock('../../../src/hooks/loader', () => ({ useLoader: jest .fn() .mockImplementation( - () => async ( - cb: () => Promise, - _: object, - onSuccess: () => void, - ) => { - await cb() - onSuccess() - }, + () => + async (cb: () => Promise, _: object, onSuccess: () => void) => { + await cb() + onSuccess() + }, ), })) @@ -99,6 +99,7 @@ describe('Correct data was set in the store for ', () => { did: COUNTERPARTY_DID, }, credentials: { + // eslint-disable-next-line service_issued: [ { type: CREDENTIAL_TYPE, diff --git a/__tests__/suits/Interactions/Scanner.test.tsx b/__tests__/suits/Interactions/Scanner.test.tsx index cd002344b..8efaab2aa 100644 --- a/__tests__/suits/Interactions/Scanner.test.tsx +++ b/__tests__/suits/Interactions/Scanner.test.tsx @@ -23,7 +23,13 @@ jest.mock('@react-navigation/native', () => ({ useNavigation: () => ({ navigate: jest.fn(), canGoBack: jest.fn().mockReturnValue(true), + goBack: jest.fn(), }), + createNavigatorFactory: jest.fn(), +})) + +jest.mock('@react-navigation/core', () => ({ + useIsFocused: jest.fn().mockReturnValue(true), })) test('it displays permission request, denies it and opens settings ', async () => { @@ -34,9 +40,9 @@ test('it displays permission request, denies it and opens settings ', async () = const { getByText } = renderWithSafeArea() - expect(getByText(strings.CAMERA_PERMISSION)).toBeDefined() + expect(getByText(/CameraPermission.header/)).toBeDefined() - fireEvent.press(getByText(strings.TAP_TO_ACTIVATE_CAMERA)) + fireEvent.press(getByText(/CameraPermission.confirmBtn/)) await waitFor(() => expect(openSettings).toHaveBeenCalledTimes(1)) }) diff --git a/__tests__/suits/LocalDeviceAuth/Lock.test.tsx b/__tests__/suits/LocalDeviceAuth/Lock.test.tsx index dbc3d3882..e8b75e79a 100644 --- a/__tests__/suits/LocalDeviceAuth/Lock.test.tsx +++ b/__tests__/suits/LocalDeviceAuth/Lock.test.tsx @@ -2,16 +2,15 @@ import React from 'react' import * as redux from 'react-redux' import { AppState } from 'react-native' -import { fireEvent, waitFor, act } from '@testing-library/react-native' +import { waitFor } from '@testing-library/react-native' import * as deviceAuthHooks from '~/hooks/deviceAuth' import Lock from '~/screens/Modals/Lock' -import { strings } from '~/translations/strings' import { setAppLocked } from '~/modules/account/actions' import { renderWithSafeArea } from '../../utils/renderWithSafeArea' import { AppStatusState } from '~/modules/appState/types' -import { ReactTestInstance } from 'react-test-renderer' import { inputPasscode } from '../../utils/inputPasscode' +import { getMockedDispatch } from '../../mocks/libs/react-redux' const mockGetBiometry = jest.fn() const mockedDispatch = jest.fn() @@ -30,6 +29,7 @@ jest.mock('../../../src/hooks/biometry', () => ({ jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), + // eslint-disable-next-line useFocusEffect: jest.fn().mockImplementation(() => {}), useNavigation: () => ({}), })) @@ -50,9 +50,7 @@ jest.mock( listeners.push(handler) } }), - removeEventListener: jest.fn((event, handler) => { - return undefined - }), + removeEventListener: jest.fn((event, handler) => undefined), emit: jest.fn((event, nextAppState: AppStatusState) => { listeners.forEach((l) => l(nextAppState)) }), @@ -60,13 +58,6 @@ jest.mock( }, ) -const getMockedDispatch = () => { - const useDispatchSpy = jest.spyOn(redux, 'useDispatch') - const mockDispatchFn = jest.fn() - useDispatchSpy.mockReturnValue(mockDispatchFn) - return mockDispatchFn -} - describe('Without biometry', () => { beforeEach(() => { const useGetDeviceAuthParamsSpy = jest.spyOn( @@ -85,9 +76,9 @@ describe('Without biometry', () => { mockGetBiometry.mockResolvedValue(undefined) - expect(getByText(strings.ENTER_YOUR_PASSCODE)).toBeDefined() + expect(getByText(/Lock.header/)).toBeDefined() + expect(getByText(/Lock.forgotBtn/)).toBeDefined() expect(getByTestId('passcode-keyboard')).toBeDefined() - expect(getByText(strings.FORGOT_YOUR_PASSCODE)).toBeDefined() }) test("The app is locked if pins don't match", async () => { @@ -96,7 +87,7 @@ describe('Without biometry', () => { inputPasscode(getByTestId, [3, 3, 3, 3]) await waitFor(() => { - expect(getByText(strings.WRONG_PASSCODE)).toBeDefined() + expect(getByText(/ChangePasscode.wrongCodeHeader/)).toBeDefined() }) }) @@ -135,12 +126,12 @@ describe('With biometry', () => { const mockDispatchFn = getMockedDispatch() - renderWithSafeArea() + await waitFor(() => renderWithSafeArea()) // ACT: this imitates the app going to the background - // @ts-ignore - because it is a custom method, and react native App State has no emit method available + // @ts-expect-error - because it is a custom method, and react native App State has no emit method available AppState.emit('change', 'background') - // @ts-ignore + // @ts-expect-error AppState.emit('change', 'active') await waitFor(() => { @@ -156,12 +147,12 @@ describe('With biometry', () => { const mockDispatchFn = getMockedDispatch() - renderWithSafeArea() + await waitFor(() => renderWithSafeArea()) // ACT: this imitates the app going to the background - // @ts-ignore + // @ts-expect-error AppState.emit('change', 'background') - // @ts-ignore + // @ts-expect-error AppState.emit('change', 'active') await waitFor(() => { diff --git a/__tests__/suits/LocalDeviceAuth/RegisterPin.test.tsx b/__tests__/suits/LocalDeviceAuth/RegisterPin.test.tsx index 1c15a473c..810fcf807 100644 --- a/__tests__/suits/LocalDeviceAuth/RegisterPin.test.tsx +++ b/__tests__/suits/LocalDeviceAuth/RegisterPin.test.tsx @@ -1,9 +1,8 @@ import React from 'react' -import { fireEvent, waitFor } from '@testing-library/react-native' +import { waitFor } from '@testing-library/react-native' import { setGenericPassword, STORAGE_TYPE } from 'react-native-keychain' import RegisterPin from '~/screens/Modals/DeviceAuthentication/RegisterPin' -import { strings } from '~/translations' import { renderWithSafeArea } from '../../utils/renderWithSafeArea' import { PIN_USERNAME, PIN_SERVICE } from '~/utils/keychainConsts' import { inputPasscode } from '../../utils/inputPasscode' @@ -14,6 +13,7 @@ const mockedDispatch = jest.fn() jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), + // eslint-disable-next-line useFocusEffect: jest.fn().mockImplementation(() => {}), useNavigation: () => ({ navigation: mockNavigation, @@ -24,44 +24,45 @@ jest.mock('@react-navigation/native', () => ({ jest.mock('react-redux', () => ({ ...jest.requireActual('react-redux'), useSelector: jest.fn(), - useDispatch: jest.fn().mockReturnValue(mockedDispatch), + useDispatch: () => mockedDispatch, })) -jest.mock('react-native-keychain', () => { - return { - STORAGE_TYPE: { - AES: 'aes', - }, - setGenericPassword: jest.fn(() => Promise.resolve(true)), - } -}) +jest.mock('react-native-keychain', () => ({ + STORAGE_TYPE: { + AES: 'aes', + }, + setGenericPassword: jest.fn(() => Promise.resolve(true)), + resetGenericPassword: jest.fn(() => Promise.resolve(true)), +})) -test('User is able to set up pin', async () => { - const { getByText, getByTestId, queryByText } = renderWithSafeArea( - , - ) +describe('Register Passcode', () => { + it('User is able to set up pin', async () => { + const { getByText, getByTestId, queryByText } = renderWithSafeArea( + , + ) - expect(getByText(strings.CREATE_PASSCODE)).toBeDefined() - expect(getByText(strings.IN_ORDER_TO_PROTECT_YOUR_DATA)).toBeDefined() - expect(queryByText(/strings.YOU_CAN_CHANGE_THE_PASSCODE/)).toBeDefined() + expect(getByText(/CreatePasscode.createHeader/)).toBeDefined() + expect(getByText(/CreatePasscode.createSubheader/)).toBeDefined() + expect(getByText(/CreatePasscode.helperText/)).toBeDefined() - inputPasscode(getByTestId, [1, 1, 1, 1]) + inputPasscode(getByTestId, [1, 1, 1, 1]) - expect(getByText(strings.VERIFY_PASSCODE)).toBeDefined() - expect(getByText(strings.ADDING_AN_EXTRA_LAYER_OF_SECURITY)).toBeDefined() + expect(getByText(/VerifyPasscode.verifyHeader/)).toBeDefined() + expect(getByText(/VerifyPasscode.verifySubheader/)).toBeDefined() - inputPasscode(getByTestId, [1, 1, 1, 2]) + inputPasscode(getByTestId, [1, 1, 1, 2]) - await waitFor(() => { - // expect input to clean up because pins don't match - expect(queryByText('*')).toBe(null) - }) + await waitFor(() => { + // expect input to clean up because pins don't match + expect(queryByText('*')).toBe(null) + }) - inputPasscode(getByTestId, [1, 1, 1, 1]) + inputPasscode(getByTestId, [1, 1, 1, 1]) - expect(setGenericPassword).toHaveBeenCalledTimes(1) - expect(setGenericPassword).toHaveBeenCalledWith(PIN_USERNAME, '1111', { - service: PIN_SERVICE, - storage: STORAGE_TYPE.AES, + expect(setGenericPassword).toHaveBeenCalledTimes(1) + expect(setGenericPassword).toHaveBeenCalledWith(PIN_USERNAME, '1111', { + service: PIN_SERVICE, + storage: STORAGE_TYPE.AES, + }) }) }) diff --git a/__tests__/suits/LoggedOut/Recovery.test.tsx b/__tests__/suits/LoggedOut/Recovery.test.tsx index d5842702a..afb4f3315 100644 --- a/__tests__/suits/LoggedOut/Recovery.test.tsx +++ b/__tests__/suits/LoggedOut/Recovery.test.tsx @@ -3,13 +3,15 @@ import * as redux from 'react-redux' import { fireEvent } from '@testing-library/react-native' import Recovery from '~/screens/Modals/Recovery' -import { strings } from '~/translations/strings' import { renderWithSafeArea } from '../../utils/renderWithSafeArea' const mockAppState = { loader: { isVisible: false, }, + account: { + screenHeight: 800, + }, } jest.useFakeTimers() @@ -44,6 +46,7 @@ jest.mock('@react-navigation/native', () => ({ useNavigation: () => ({ dispatch: jest.fn(), goBack: jest.fn(), + canGoBack: jest.fn(() => true), }), })) @@ -59,17 +62,17 @@ describe('User on a Recovery screen', () => { const useDispatchSpy = jest.spyOn(redux, 'useDispatch') const mockDispatchFn = jest.fn() useDispatchSpy.mockReturnValue(mockDispatchFn) - // @ts-ignore - redux.useSelector.mockImplementation((callback: (state: any) => void) => { - return callback(mockAppState) - }) + // @ts-expect-error + redux.useSelector.mockImplementation((callback: (state: unknown) => void) => + callback(mockAppState), + ) const { getByText, getByTestId } = renderWithSafeArea() - expect(getByText(strings.RECOVERY)).toBeDefined() - expect(getByText(strings.START_ENTERING_SEED_PHRASE)).toBeDefined() + expect(getByText(/Recovery.header/)).toBeDefined() + expect(getByText(/Recovery.subheader/)).toBeDefined() expect(getByTestId('seedphrase-input')).toBeDefined() - expect(getByText(strings.WHAT_IF_I_FORGOT)).toBeDefined() - expect(getByText(strings.CONFIRM)).toBeDefined() - expect(getByText(strings.BACK)).toBeDefined() + expect(getByText(/Recovery.forgotBtn/)).toBeDefined() + expect(getByText(/Recovery.confirmBtn/)).toBeDefined() + expect(getByText(/Recovery.exitBtn/)).toBeDefined() }) test('can add a seed key to a phrase', async () => { diff --git a/__tests__/suits/components/Cards/ScaledCard.test.tsx b/__tests__/suits/components/Cards/ScaledCard.test.tsx new file mode 100644 index 000000000..a1f544052 --- /dev/null +++ b/__tests__/suits/components/Cards/ScaledCard.test.tsx @@ -0,0 +1,86 @@ +import React from 'react' +import { Dimensions } from 'react-native' +import { fireEvent, render } from '@testing-library/react-native' + +import ScaledCard, { + ScaledText, + ScaledView, +} from '~/components/Cards/ScaledCard' +import { + handleContainerLayout, + scaleStyleObject, +} from '~/components/Cards/ScaledCard/utils' + +jest.mock('react-native/Libraries/Utilities/Dimensions.js', () => ({ + get: jest.fn(), +})) +jest.mock('react-native/Libraries/Utilities/PixelRatio.js') +jest.mock('../../../../src/components/Cards/ScaledCard/utils.ts', () => ({ + ...jest.requireActual('../../../../src/components/Cards/ScaledCard/utils.ts'), + handleContainerLayout: jest.fn(), +})) + +const mockDeviceScreenWidth = 200 + +it('scales styles given scaleBy value', () => { + const scaledStyles = scaleStyleObject( + [{ width: 100 }, { paddingBottom: 100 }], + 0.5, + ) + expect(scaledStyles.width).toBe(50) + expect(scaledStyles.paddingBottom).toBe(50) +}) + +describe('ScaledCard', () => { + // @ts-expect-error + Dimensions.get.mockReturnValue({ width: mockDeviceScreenWidth }) + + it('calculates scaled properties based on original screen width', () => { + const { getByTestId } = render( + + + + , + ) + const scaledview = getByTestId('scaled-View') + const scaledtext = getByTestId('scaled-Text') + expect(scaledview.props.style[0]).toEqual({ flex: 0.5 }) + expect(scaledview.props.style[1]).toEqual({ width: 25 }) + expect(scaledtext.props.style[1]).toEqual({ fontSize: 10 }) + }) + + it('calculates scaled properties based on container width', () => { + // @ts-expect-error + handleContainerLayout.mockReturnValue({ width: 100, height: 50 }) + const { getByTestId } = render( + + + + , + ) + + const scaledview = getByTestId('scaled-View') + const scaledtext = getByTestId('scaled-Text') + /** + * values are not scaled because onlayout event wasn't + * called and containerWidth is undefined, therefore, + * it falls back to scaleBy 1 + */ + expect(scaledview.props.style[0]).toEqual({ flex: 0.5 }) + expect(scaledview.props.style[1]).toEqual({ width: 50 }) + expect(scaledtext.props.style[1]).toEqual({ fontSize: 20 }) + + const container = getByTestId('scaled-container') + fireEvent(container, 'layout') + + expect(handleContainerLayout).toBeCalledTimes(1) + + expect(scaledview.props.style[0]).toEqual({ flex: 0.5 }) + expect(scaledview.props.style[1]).toEqual({ width: 25 }) + expect(scaledtext.props.style[1]).toEqual({ fontSize: 10 }) + }) +}) diff --git a/__tests__/suits/components/Cards/getCardDimensions.test.ts b/__tests__/suits/components/Cards/getCardDimensions.test.ts new file mode 100644 index 000000000..59c1cb211 --- /dev/null +++ b/__tests__/suits/components/Cards/getCardDimensions.test.ts @@ -0,0 +1,128 @@ +import { Dimensions } from 'react-native' +import { getCardDimensions } from '~/components/Cards/getCardDimenstions' + +const mockBaseCardWidth = 300 +const mockBaseCardHeight = 200 +const mockBaseScreenWidth = 500 + +const mockDeviceScreenWidth = 350 +/** + * NOTE: this variable is used to test when device screen width + * is larger than base screen width + */ +const mockDeviceScreenWidthModifier = 1.4 +const mockDeviceScreenWidthLarge = + mockBaseScreenWidth * mockDeviceScreenWidthModifier + +const mockAvailableWidth = 280 +/** + * NOTE: this variable is used to test when available width + * is larger than base card width + */ +const mockLargeWidthModifier = 1.2 +const mockAvailableWidthLarge = mockBaseCardWidth * mockLargeWidthModifier + +/** + * NOTE: this mock is volatile because if path to + * Dimenstion class changes we need to update path here + */ +jest.mock('react-native', () => ({ + Dimensions: { get: jest.fn() }, +})) + +describe('getCardDimensions', () => { + describe('runs tests for screens smaller than base screen', () => { + beforeAll(() => { + // @ts-expect-error + Dimensions.get.mockReturnValue({ width: mockDeviceScreenWidth }) + }) + + afterAll(() => { + // @ts-expect-error + Dimensions.get.mockReset() + }) + + it("throws an error if defining option argument wasn't used correctly", () => { + expect(() => { + // @ts-expect-error + getCardDimensions(320, 398, { + originalScreenWidth: 100, + containerWidth: 300, + }) + }).toThrowError( + new Error( + '"definigOption" param in getCardDimensions fn was used incorrectly', + ), + ) + expect(() => { + // @ts-expect-error + getCardDimensions(320, 398, {}) + }).toThrowError( + new Error( + '"definigOption" param in getCardDimensions fn was used incorrectly', + ), + ) + }) + + it('calculates card dimensions based on originalScreenWidth defining property', () => { + const { scaledWidth, scaledHeight, scaleBy } = getCardDimensions( + mockBaseCardWidth, + mockBaseCardHeight, + { originalScreenWidth: mockBaseScreenWidth }, + ) + expect(scaleBy).toBe(mockDeviceScreenWidth / mockBaseScreenWidth) + expect(scaledWidth).toBe( + (mockDeviceScreenWidth / mockBaseScreenWidth) * mockBaseCardWidth, + ) + expect(scaledHeight).toBe( + mockBaseCardHeight * (mockDeviceScreenWidth / mockBaseScreenWidth), + ) + }) + it('calculates card dimensions based on containerWidth defining property: containerWidth is smaller than baseCardWidth', () => { + const { scaledWidth, scaledHeight, scaleBy } = getCardDimensions( + mockBaseCardWidth, + mockBaseCardHeight, + { containerWidth: mockAvailableWidth }, + ) + + expect(scaleBy).toBe(mockAvailableWidth / mockBaseCardWidth) + expect(scaledWidth).toBe(mockAvailableWidth) + expect(scaledHeight).toBe( + mockBaseCardHeight * (mockAvailableWidth / mockBaseCardWidth), + ) + }) + it('calculates card dimensions based on containerWidth defining property: containerWidth is larger than baseCardWidth', () => { + const { scaledWidth, scaledHeight, scaleBy } = getCardDimensions( + mockBaseCardWidth, + mockBaseCardHeight, + { containerWidth: mockAvailableWidthLarge }, + ) + + expect(scaleBy).toBe(mockLargeWidthModifier) + expect(scaledWidth).toBe(mockBaseCardWidth * mockLargeWidthModifier) + expect(scaledHeight).toBe(mockBaseCardHeight * mockLargeWidthModifier) + }) + }) + describe('runs tests for screens larger than base screen', () => { + beforeAll(() => { + // @ts-expect-error + Dimensions.get.mockReturnValue({ width: mockDeviceScreenWidthLarge }) + }) + + it('calculates card dimensions based on originalScreenWidth defining property', () => { + const { scaledWidth, scaledHeight, scaleBy } = getCardDimensions( + mockBaseCardWidth, + mockBaseCardHeight, + { originalScreenWidth: mockBaseScreenWidth }, + ) + + expect(scaleBy).toBe(mockDeviceScreenWidthModifier) + expect(scaledWidth).toBe( + mockBaseCardWidth * mockDeviceScreenWidthModifier, + ) + expect(scaledHeight).toBe( + mockBaseCardHeight * mockDeviceScreenWidthModifier, + ) + }) + }) +}) diff --git a/__tests__/suits/components/Passcode.test.tsx b/__tests__/suits/components/Passcode.test.tsx index 1eae064ad..6df0a2ac5 100644 --- a/__tests__/suits/components/Passcode.test.tsx +++ b/__tests__/suits/components/Passcode.test.tsx @@ -6,7 +6,6 @@ import { cleanup, } from '@testing-library/react-native' import Passcode from '~/components/Passcode' -import { strings } from '~/translations' import { ScreenNames } from '~/types/screens' import { inputPasscode } from '../../utils/inputPasscode' @@ -15,6 +14,7 @@ const mockNavigate = jest.fn() jest.mock('@react-navigation/native', () => ({ ...jest.requireActual('@react-navigation/native'), + // eslint-disable-next-line useFocusEffect: jest.fn().mockImplementation(() => {}), useNavigation: () => ({ navigate: mockNavigate, @@ -58,20 +58,20 @@ describe('Passcode', () => { const { getByText, getByTestId } = render( , ) - expect(getByText(strings.CREATE_PASSCODE)).toBeDefined() + expect(getByText(/CreatePasscode.createHeader/)).toBeDefined() inputPasscode(getByTestId, [1, 1, 1, 1]) await waitFor(() => { - expect(getByText(strings.WRONG_PASSCODE)).toBeDefined() + expect(getByText(/ChangePasscode.wrongCodeHeader/)).toBeDefined() }) }) @@ -83,7 +83,7 @@ describe('Passcode', () => { , ) - expect(getByText(strings.FORGOT_YOUR_PASSCODE)).toBeDefined() + expect(getByText(/Lock.forgotBtn/)).toBeDefined() const forgotBtn = getByTestId('button') fireEvent.press(forgotBtn) diff --git a/__tests__/suits/components/Tabs.test.tsx b/__tests__/suits/components/Tabs.test.tsx index b9d653fb7..cd852dee5 100644 --- a/__tests__/suits/components/Tabs.test.tsx +++ b/__tests__/suits/components/Tabs.test.tsx @@ -3,63 +3,7 @@ import { fireEvent } from '@testing-library/react-native' import Documents from '~/screens/LoggedIn/Documents' import { renderWithSafeArea } from '../../utils/renderWithSafeArea' import { Colors } from '~/utils/colors' - -const mockedDocuments = { - documents: [ - { - id: 'abscbajhfjdhfjdshfsdjhfa', - type: 'document', - claim: { - id: 'id1', - message: 'message1', - }, - metadata: { - name: 'Document 1', - }, - issuer: { - did: 'did:jun:example', - }, - }, - { - id: 'dsfjsjdfjhdfasjdhfasdhjfajsdhf', - type: 'document 2', - claim: { - id: 'id2', - message: 'message2', - }, - metadata: { - name: 'Document 2', - }, - issuer: { - did: 'did:jun:example', - publicProfile: { - name: 'Issuer name', - description: 'I am the issuer', - }, - }, - }, - ], - other: [ - { - id: 'adfdjfahdfahdfajsdhf dfye', - type: 'other', - claim: { - id: 'id3', - message: 'message3', - }, - metadata: { - name: 'Document 3', - }, - issuer: { - did: 'did:jun:example', - publicProfile: { - name: 'Issuer name', - description: 'I am the issuer', - }, - }, - }, - ], -} +import { mockedDocuments } from '../../mocks/documents' const getColorValue = (styles: Array>) => { const value = styles.find((stylesEl) => !!stylesEl.color) diff --git a/__tests__/suits/components/__snapshots__/ErrorFallback.test.tsx.snap b/__tests__/suits/components/__snapshots__/ErrorFallback.test.tsx.snap index f4246227a..7486c2817 100644 --- a/__tests__/suits/components/__snapshots__/ErrorFallback.test.tsx.snap +++ b/__tests__/suits/components/__snapshots__/ErrorFallback.test.tsx.snap @@ -10,7 +10,6 @@ exports[`ErrorFallback should match the initial snapshot 1`] = ` } > + { - const mockDispatchFn = jest.fn() - const useDispatchSpy = jest.spyOn(redux, 'useDispatch') - useDispatchSpy.mockReturnValue(mockDispatchFn) - return mockDispatchFn -} diff --git a/__tests__/utils/mockedValues.ts b/__tests__/utils/mockedValues.ts deleted file mode 100644 index de9ae530b..000000000 --- a/__tests__/utils/mockedValues.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { FlowType } from '@jolocom/sdk' - -export const mockedAgent = (value: any) => { - return { - current: { - processJWT: jest - .fn() - .mockImplementationOnce(() => Promise.resolve(value)), - }, - } -} - -export const mockedInteractionCredOffer = { - id: '123', - flow: { - type: FlowType.CredentialOffer, - }, - getSummary: jest.fn().mockReturnValue({ - initiator: { - did: 'did123', - }, - state: { - offerSummary: [ - { - type: 'FirstCredential', - renderInfo: {}, - }, - ], - }, - }), -} - -export const mockedInteractionCredShare = { - id: '123', - flow: { - type: FlowType.CredentialShare, - }, - getSummary: jest.fn().mockReturnValue({ - initiator: { - did: 'did123', - }, - state: { - constraints: [ - { - requestedCredentialTypes: [ - ['Credential', 'ProofOfEmailCredential'], - ['Credential', 'ProofOfNameCredential'], - ['Credential', 'DemoCred'], - ], - }, - ], - }, - }), -} - -export const mockedInteractionAuth = { - id: '123', - flow: { - type: FlowType.Authentication, - }, - getSummary: jest.fn().mockReturnValue({ - initiator: { - did: 'did123', - }, - state: { - description: 'Is it really you ?', - }, - }), -} - -export const mockedInteractionAuthz = { - id: '123', - flow: { - type: FlowType.Authorization, - }, - getSummary: jest.fn().mockReturnValue({ - initiator: { - did: 'did123', - }, - state: { - description: 'Service would like you to', - imageURL: 'adjakjda', - action: 'Unlock', - }, - }), -} - -export const mockedNoCredentials = { - credentials: { - all: [], - }, -} - -export const mockedHasCredentials = { - credentials: { - all: [{ type: 'DemoCred' }], - }, -} diff --git a/__tests__/utils/selector.ts b/__tests__/utils/selector.ts deleted file mode 100644 index 67a711bd0..000000000 --- a/__tests__/utils/selector.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { useSelector } from 'react-redux' - -type MockedStore = Record | MockedStore - -export function mockSelectorReturn(mockedStore: MockedStore) { - // @ts-expect-error - useSelector.mockImplementation((callback: (state: any) => void) => { - return callback(mockedStore) - }) -} diff --git a/__tests__/utils/setup.ts b/__tests__/utils/setup.ts index 8a106ac74..8632003cd 100644 --- a/__tests__/utils/setup.ts +++ b/__tests__/utils/setup.ts @@ -1,3 +1,5 @@ +import { NativeModules } from 'react-native' + jest.mock('react-native-keychain', () => ({ SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY', SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE', @@ -35,7 +37,7 @@ jest.mock('react-native-jolocom', () => ({ })) jest.mock('react-native-localize', () => ({ - findBestAvailableLanguage: (_: Array) => 'en', + findBestAvailableLanguage: (_: string[]) => 'en', })) jest.mock('../../src/errors/errorContext.tsx', () => ({ @@ -62,7 +64,7 @@ jest.mock( jest.mock('react-native-gesture-handler', () => { const gestureHandlerMocks = jest.requireActual( - '../../node_modules/react-native-gesture-handler/__mocks__/RNGestureHandlerModule.js', + '../../node_modules/react-native-gesture-handler/src/mocks.ts', ).default return { @@ -72,5 +74,25 @@ jest.mock('react-native-gesture-handler', () => { } }) -// @ts-ignore +jest.mock('../../src/hooks/useTranslation.ts', () => () => ({ + currentLanguage: 'en', + t: jest + .fn() + .mockImplementation( + (term, interpolationValue) => term + interpolationValue, + ), +})) + +// @ts-expect-error global.__reanimatedWorkletInit = jest.fn() + +NativeModules.RNCNetInfo = { + getCurrentState: jest.fn(() => Promise.resolve()), + addListener: jest.fn(), + removeListeners: jest.fn(), +} + +jest.mock('react-native-keyboard-aware-scroll-view', () => { + const KeyboardAwareScrollView = ({ children }: { children: any }) => children + return { KeyboardAwareScrollView } +}) diff --git a/android/app/build.gradle b/android/app/build.gradle index d1f8f3a5b..e4354454e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -130,12 +130,16 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + dexOptions { + javaMaxHeapSize "4g" + } + defaultConfig { applicationId "com.jolocomwallet" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 34 - versionName "2.0.0" + versionCode 37 + versionName "2.1.0" multiDexEnabled true missingDimensionStrategy 'react-native-camera', 'general' } diff --git a/android/app/src/main/java/com/jolocomwallet/MainActivity.java b/android/app/src/main/java/com/jolocomwallet/MainActivity.java index 14ec53e2c..3a59150c7 100644 --- a/android/app/src/main/java/com/jolocomwallet/MainActivity.java +++ b/android/app/src/main/java/com/jolocomwallet/MainActivity.java @@ -13,7 +13,7 @@ public class MainActivity extends ReactActivity { */ @Override protected String getMainComponentName() { - return "JolocomWallet"; + return "SmartWallet"; } @Override diff --git a/android/gradle.properties b/android/gradle.properties index 3bdbd3d4e..f966a3731 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -24,5 +24,8 @@ android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true +# Increase heap size +org.gradle.jvmargs=-Xmx4608M + # Version of flipper SDK to use with React Native FLIPPER_VERSION=0.54.0 diff --git a/android/settings.gradle b/android/settings.gradle index dd4e92832..9b2e4b825 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,4 @@ -rootProject.name = 'JolocomWallet' +rootProject.name = 'SmartWallet' include ':react-native-linear-gradient' project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android') apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings) diff --git a/app.json b/app.json index 1ddbfb522..88ecbe2b4 100644 --- a/app.json +++ b/app.json @@ -1,4 +1,4 @@ { - "name": "JolocomWallet", + "name": "SmartWallet", "displayName": "JolocomWallet" } diff --git a/bin/commit/formatter.js b/bin/commit/formatter.js new file mode 100644 index 000000000..43a73c50f --- /dev/null +++ b/bin/commit/formatter.js @@ -0,0 +1,15 @@ +module.exports = function (results) { + const conciseResult = results.reduce( + (acc, v) => { + acc.errorCount += v.errorCount + acc.files += v.filePath + ' ' + return acc + }, + { errorCount: 0, files: '' }, + ) + if (conciseResult.errorCount !== 0) { + return conciseResult.files + } else { + return '' + } +} diff --git a/bin/commit/utils.ts b/bin/commit/utils.ts new file mode 100644 index 000000000..550dd34fc --- /dev/null +++ b/bin/commit/utils.ts @@ -0,0 +1,49 @@ +import childProcess from 'child_process' + +export const listStagedFiles = () => + childProcess.execSync(`git diff --cached --name-only --diff-filter=ACMR`) + +export const abortScript = (msg: string) => { + console.log('') + console.log('\x1b[0;33m%s\x1b[0m', msg) + console.log('\x1b[0;31m%s\x1b[0m', 'Aborting') + process.exit(1) +} + +export const spawnProcess = ( + onClose: ( + code: number | null, + dataOutput: string, + errorOutput: string, + ) => void, + cmd: string, + args?: readonly string[], + options?: childProcess.SpawnOptionsWithoutStdio, +) => { + const spawnedProcess = childProcess.spawn(cmd, args, options) + + let dataOutput = '' + let errorOutput = '' + + spawnedProcess.stdout.on('data', (data) => { + dataOutput += data.toString() + '\n' + }) + spawnedProcess.stderr.on('data', (error: Buffer) => { + errorOutput += error.toString() + '\n' + }) + + /** + * NOTE: this is for debug mode to see the original output of processes + */ + if (process.env.DEBUG) { + spawnedProcess.stderr.pipe(process.stderr) + spawnedProcess.stdout.pipe(process.stdout) + } + + spawnedProcess.on('close', (code) => onClose(code, dataOutput, errorOutput)) + + return spawnedProcess +} + +export const formatOutput = (output: string): string[] => + output.split('\n').filter((o) => Boolean(o)) diff --git a/bin/precommit.js b/bin/precommit.js deleted file mode 100644 index 315144ebd..000000000 --- a/bin/precommit.js +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env node - -const child_process = require('child_process'); - -// check that nothing is committed in android/gradle.properties -const gradlePropertiesDiff = child_process.execSync( - 'git diff --cached --raw ./android/gradle.properties', -); - -if (gradlePropertiesDiff != '') { - console.error('android/gradle.properties should not be committed!'); - process.exit(1); -} diff --git a/bin/precommit.ts b/bin/precommit.ts new file mode 100644 index 000000000..0a0af6565 --- /dev/null +++ b/bin/precommit.ts @@ -0,0 +1,180 @@ +#!/usr/bin/env node + +import { + abortScript, + formatOutput, + listStagedFiles, + spawnProcess, +} from './commit/utils' +import EventEmitter from 'events' +import { Table } from 'console-table-printer' + +type ChildProcessVariants = 'local.properties' | 'prettier' | 'linter' +type ResultOption = { isFinished: boolean; passed: boolean; cmd: string } +type Result = Record + +const result: Result = { + ['local.properties']: { + isFinished: false, + passed: false, + cmd: '', + }, + ['prettier']: { + isFinished: false, + passed: false, + cmd: '', + }, + ['linter']: { + isFinished: false, + passed: false, + cmd: '', + }, +} + +const eventEmmiter = new EventEmitter() +eventEmmiter.on( + 'process-finished', + (processName: ChildProcessVariants, passed: boolean, cmd: string) => { + result[processName].isFinished = true + result[processName].passed = passed + result[processName].cmd = cmd + console.log('\x1b[0;33m%s\x1b[0m', `Finished running ${processName}`) + const allFinished = Object.keys(result).every( + (k) => result[k as ChildProcessVariants].isFinished, + ) + if (allFinished) { + const allPassed = Object.keys(result).every( + (k) => result[k as ChildProcessVariants].passed, + ) + + if (!allPassed) { + const t = new Table({ + columns: [ + { + name: 'index', + title: 'Process', + alignment: 'right', + color: 'blue', + }, + { name: 'isFinished', title: 'Has finished?', alignment: 'right' }, + { name: 'passed', title: 'Did check pass?', alignment: 'right' }, + { + name: 'cmd', + title: 'Fix it by the following instruction', + alignment: 'left', + }, // with Title as separate Text + ], + }) + Object.keys(result).forEach((p) => { + const process = p as ChildProcessVariants + t.addRow( + { index: p, ...result[process] }, + { color: result[process].passed ? 'green' : 'red' }, + ) + }) + t.printTable() + abortScript('Not all checks passed') + } else { + console.log('') + console.log('\x1b[0;42m%s\x1b[0m', 'All checks passed') + console.log('') + } + } + }, +) + +const checkStagedLocalProp = (files: string[]) => { + const localPropertiesFiles = files.filter((p) => + /(local\.properties)/.exec(p), + ) + eventEmmiter.emit( + 'process-finished', + 'local.properties', + !localPropertiesFiles.length, + !localPropertiesFiles.length + ? '' + : `Remove local.properties file from staged area`, + ) +} + +const prettifyFiles = (files: string[]) => { + const handleProcessClose = ( + code: number | null, + dataOutput: string, + errorOutput: string, + ) => { + if (code === 1) { + const erroredFiles = formatOutput(errorOutput) + .slice(0, -1) + .map((o) => o.replace(/^\[warn]/, '').trim()) + .join(' ') + eventEmmiter.emit( + 'process-finished', + 'prettier', + false, + `yarn prettier:format ${erroredFiles}`, + ) + } else if (code === 2) { + abortScript('Prettier check failed. Something is wrong with prettier') + } else { + eventEmmiter.emit('process-finished', 'prettier', true, '') + } + } + spawnProcess(handleProcessClose, 'npm', ['run', 'prettier:check', ...files]) +} + +const lintFiles = (files: string[]) => { + const handleProcessClose = ( + code: number | null, + dataOutput: string, + errorOutput: string, + ) => { + const output = formatOutput(dataOutput) + let erroredFiles: string + if (output.length > 2) { + erroredFiles = output[output.length - 1] + .split(' ') + .filter((f) => Boolean(f)) + .join(' ') + eventEmmiter.emit( + 'process-finished', + 'linter', + false, + `yarn lint:fix ${erroredFiles}`, + ) + } else { + eventEmmiter.emit('process-finished', 'linter', true, '') + } + } + spawnProcess(handleProcessClose, 'npm', ['run', 'lint:check', ...files]) +} + +const main = () => { + try { + console.log('\x1b[0;34m%s\x1b[0m', 'Running check for:') + console.log('\x1b[0;34m%s\x1b[0m', '\t - staged local.propeties') + console.log('\x1b[0;34m%s\x1b[0m', '\t - prettier') + console.log('\x1b[0;34m%s\x1b[0m', '\t - linter') + const stagedFiles = listStagedFiles().toString('utf-8') + if (stagedFiles === '') { + throw new Error('No files staged') + } + const formattedStagedFiles = stagedFiles + .split('\n') + .filter((path) => Boolean(path)) + + checkStagedLocalProp(formattedStagedFiles) + + const onlyJSTSFiles = formattedStagedFiles.filter((f) => + /\.(ts|tsx|js|jsx)$/g.exec(f), + ) + prettifyFiles(onlyJSTSFiles) + lintFiles(onlyJSTSFiles) + } catch (e: unknown) { + if (e instanceof Error) { + abortScript(e.message) + } + } +} + +main() diff --git a/bin/terms/add-term.ts b/bin/terms/add-term.ts new file mode 100644 index 000000000..7639361d5 --- /dev/null +++ b/bin/terms/add-term.ts @@ -0,0 +1,123 @@ +import minimist from 'minimist'; +import inquirer from 'inquirer'; + +import { cloneSecrets, Languages, sendPostRequest } from './utils'; +import { questionsAddingTerm, questionUpdateTranslation } from './questions'; + +const args = minimist(process.argv.slice(2), { + string: ["ctx", "term", "content", "lang"], + boolean: ["help"] +}) + +let ctx = args.ctx, + term = args.term, + content = args.content, + lang = args.lang || Languages.en + +const printHelp = () => { + console.log(""); + console.log('\x1b[4;95m%s\x1b[0m', 'Script usage:') + console.log('\x1b[0;95m%s\x1b[0m', 'yarn term:add --ctx=Term --term=testTerm --content=testing --lang=de'); + console.log(""); + console.log("--help prints help"); + console.log("--ctx (required) term context, i.e. Walkthrough"); + console.log("--term (required) term label"); + console.log("--content (required) term value"); + console.log("--lang (optional) 'en' | 'de' - term translation language, defaults to 'en'"); +} + +const addTerm = async () => { + const params = { + data: JSON.stringify([ + { + term, + context: ctx, + comment: `Translations missing` + } + ]) + } + const data = await sendPostRequest('/terms/add', params); + if(data && data.response.code === "200" && data.result.terms.added === 1) { + return data; + } else if(data && data.response.code === "200" && data.result.terms.added === 0) { + // term already exists + return data; + } else { + throw `Term "${term}" could not added` + } +} + +const addTranslation = async (mode: 'add' | 'update' = 'add') => { + if(lang !== Languages && mode === 'add') { + console.log(""); + console.log('\x1b[0;93m%s\x1b[0m', `Provided lang argument "${lang}" is not supported.`); + console.log('\x1b[0;93m%s\x1b[0m', `Falling back to "${Languages.en}".`); + console.log(""); + lang = Languages.en; + } + const params = { + language: lang, + data: JSON.stringify([ + { + term, + context: ctx, + translation: { + content + } + } + ]) + } + const data = await sendPostRequest(`/translations/${mode}`, params); + const key = `${mode}${mode === 'add' ? 'ed' : 'd'}`; + if(data && data.response.code === "200" && data.result.translations[key] === 1) { + return data; + } else if(data && data.response.code === "200" && data.result.translations[key] === 0) { + if(mode === 'add') { + const {update: shouldUpdate} = await inquirer.prompt(questionUpdateTranslation); + if(shouldUpdate) { + // overwrite translation + await addTranslation('update') + } + return data; + } + return; + } else { + throw `Translation "${content}" could not be added` + } +} + +const main = async () => { + try { + await cloneSecrets(); + await addTerm(); + await addTranslation(); + await import('./import-terms'); + } catch(err) { + console.log('\x1b[0;91m%s\x1b[0m', err) + } +} + +if(args.help) { + printHelp(); + process.exit(0); +} + +if(!ctx || !term || !content) { + console.log('\x1b[4;91m%s\x1b[0m', 'Missing required params') + printHelp(); + console.log(""); + console.log('\x1b[4;92m%s\x1b[0m', 'Let\'s fill in missing arguments'); + const updatedQuestions = questionsAddingTerm.filter(q => !Boolean(args[q.name])); + (async() => { + const answers = await inquirer.prompt(updatedQuestions); + ctx = args.ctx || answers.ctx; + term = args.term || answers.term; + content = args.content || answers.content; + await main(); + })(); +} else { + (async () => { + await main(); + })() +} + diff --git a/bin/terms/import-terms.ts b/bin/terms/import-terms.ts new file mode 100644 index 000000000..30146a697 --- /dev/null +++ b/bin/terms/import-terms.ts @@ -0,0 +1,48 @@ +import fetch from 'node-fetch' +import fs from 'fs' +import { cloneSecrets, IResponse, Languages, sendPostRequest } from './utils' + +const CURRENT_PATH = process.cwd() +const TRANSLATIONS_LOCATION = `${CURRENT_PATH}/src/translations/` + +const downloadTerms = (url: string) => fetch(url).then((res) => res.json()) + +const saveTerms = (name: string, terms: unknown) => { + fs.writeFile( + `${TRANSLATIONS_LOCATION}/${name}`, + JSON.stringify(terms, null, 4), + (err) => { + if (err) { + throw err + } + }, + ) +} + +const assembleTermsForLanguage = (language: Languages) => + sendPostRequest>>('/projects/export', { + language, + type: 'key_value_json', + }) + .then((res) => { + if (res) return res.result.url as string + else throw new Error('Url not found') + }) + .then(downloadTerms) + .then((terms) => saveTerms(`${language}.json`, terms)) + .then(() => + console.log(`Successfully saved ${language.toUpperCase()} terms`), + ) + .catch((e) => { + console.warn(`Failed getting terms for ${language.toUpperCase()}`, e) + }) + +const main = async () => { + await cloneSecrets().then(console.log) + await assembleTermsForLanguage(Languages.en) + await assembleTermsForLanguage(Languages.de) +} + +;(async () => { + main() +})() diff --git a/bin/terms/questions.ts b/bin/terms/questions.ts new file mode 100644 index 000000000..7fb651138 --- /dev/null +++ b/bin/terms/questions.ts @@ -0,0 +1,26 @@ +export const questionsAddingTerm = [ + { + type: 'input', + name: 'term', + message: "What's the name of the term?", + }, + { + type: 'input', + name: 'ctx', + message: "What's the term's context?", + }, + { + type: 'input', + name: 'content', + message: "What's the term's copy?", + }, +] + +export const questionUpdateTranslation = [ + { + type: 'confirm', + name: 'update', + message: + 'Seems like a translation for the provided term is already present. Would you like to overwrite it?', + }, +] diff --git a/bin/terms/utils.ts b/bin/terms/utils.ts new file mode 100644 index 000000000..a80720e24 --- /dev/null +++ b/bin/terms/utils.ts @@ -0,0 +1,87 @@ +import fetch from 'node-fetch'; +import fs from 'fs'; +import shell from 'shelljs' + +const CURRENT_PATH = process.cwd(); +const SECRETS_REPO = 'dev@hetz1.jolocom.io:poeditor-key' +const BASE_URL = 'https://api.poeditor.com/v2'; +const API_KEY_LOCATION = `${CURRENT_PATH}/bin/poeditor-key/POEDITOR_TOKEN.txt` +const PROJECT_ID_LOCATION = `${CURRENT_PATH}/bin/poeditor-key/POEDITOR_SW2_PROJECT_ID.txt` + +export enum Languages { + en = 'en', + de = 'de', +} + +interface IResult { + [key: string]: { + [key: string]: number + } +} + +export interface IResponse { + response: { + status: string, + code: string, + message: string, + }, + result: T +} + +export const cloneSecrets = () => { + return new Promise((res, rej) => { + const keyExists = fs.existsSync(API_KEY_LOCATION); + const idExists = fs.existsSync(PROJECT_ID_LOCATION); + if(!keyExists || !idExists) { + shell.cd(`${CURRENT_PATH}/bin`) + shell.rm('-rf', `poeditor-key`) + shell.exec( + `git clone ${SECRETS_REPO} -b master`, + {}, + (code, stdout, stderr) => { + if (code != 0) return rej(new Error(stderr)) + return res(stdout) + }, + ) + } else { + res('Secrets are already present'); + } + }) +} + +const readTextFile = (location: string) => { + return new Promise((res, rej) => { + fs.readFile(location, 'utf8', (err, data) => { + if (err) rej(err) + else res(data.toString().trim()) + }) + }) +} + +const getPoeditorSecrets = async () => { + const apiKey = await readTextFile(API_KEY_LOCATION) + const projectId = await readTextFile(PROJECT_ID_LOCATION) + return { apiKey, projectId } +} + + +export const sendPostRequest = async (path: string, params: Record) => { + try { + const { apiKey, projectId } = await getPoeditorSecrets(); + const requestParams = new URLSearchParams({ + api_token: apiKey, + id: projectId, + ...params + }) + const res = await fetch(`${BASE_URL}${path}`, { + method: 'POST', + body: requestParams.toString(), + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) + return await res.json() as T; + } catch(err) { + console.error(err); + } +} \ No newline at end of file diff --git a/fastlane/Fastfile b/fastlane/Fastfile index e3df4bc32..905e97bba 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -182,7 +182,7 @@ platform :android do desc 'Build Android application and upload to Appcenter' lane :alpha do sentry_auth - build(local: true) + build api_token = get_appcenter_token.strip appcenter_upload( api_token: api_token, diff --git a/ios/SmartWallet.xcodeproj/project.pbxproj b/ios/SmartWallet.xcodeproj/project.pbxproj index 329584184..4e3291ec3 100644 --- a/ios/SmartWallet.xcodeproj/project.pbxproj +++ b/ios/SmartWallet.xcodeproj/project.pbxproj @@ -631,7 +631,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = 95S8NJ5S67; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -641,7 +641,7 @@ INFOPLIST_FILE = SmartWallet/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -664,13 +664,13 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 12; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = 95S8NJ5S67; ENABLE_BITCODE = NO; INFOPLIST_FILE = SmartWallet/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 2.0.0; + MARKETING_VERSION = 2.1.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/jest.config.js b/jest.config.js index f7f915a74..c95ef6d20 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,12 +3,13 @@ module.exports = { setupFiles: [ './__tests__/utils/setup.ts', './node_modules/react-native-gesture-handler/jestSetup.js', - './node_modules/react-native-reanimated/src/reanimated2/jestUtils.js', + // './node_modules/react-native-reanimated/src/reanimated2/jestUtils.js', ], setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], testPathIgnorePatterns: [ './node_modules/.*', './__tests__/utils/.*', + './__tests__/mocks/.*', './src/assets/svg/', '/node_modules/(?!(react-native|@sentry/react-native)/)', ], diff --git a/package.json b/package.json index 2c52d4fbd..1fc5681f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "SmartWallet", - "version": "2.0.0", + "version": "2.1.0", "private": true, "scripts": { "run:ios": "react-native run-ios", @@ -15,9 +15,13 @@ "beta:android": "bundle exec fastlane android beta", "release:android": "bundle exec fastlane android release", "start": "adb reverse tcp:8081 tcp:8081 & react-native start", + "term:add": "ts-node ./bin/terms/add-term.ts", + "terms:import": "ts-node ./bin/terms/import-terms.ts", "test-coverage": "jest --coverage --collectCoverageFrom=src/**/*.ts? --coverageDirectory=coverage", "test": "jest", "lint": "eslint . --ext .js,.jsx,.ts,.tsx", + "lint:check": "eslint --ext .js,.jsx,.ts,.tsx -f ./bin/commit/formatter.js", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx -f table --fix", "prettier:check": "prettier --check", "prettier:checkAll": "prettier --check './**/*.ts|*.tsx'", "prettier:format": "prettier --write", @@ -46,6 +50,7 @@ "assert": "^1.1.1", "asyncstorage-down": "^4.2.0", "bip39": "^3.0.2", + "console-table-printer": "^2.9.0", "crypto-browserify": "^3.12.0", "events": "^3.1.0", "formik": "^2.2.6", @@ -108,12 +113,16 @@ "@react-native-community/eslint-config": "^1.1.0", "@testing-library/jest-native": "^3.3.0", "@testing-library/react-native": "^7.0.2", + "@types/inquirer": "^7.3.3", "@types/jest": "^24.0.24", + "@types/minimist": "^1.2.2", + "@types/node-fetch": "^2.5.12", "@types/react-native": "^0.62.0", "@types/react-native-snap-carousel": "^3.8.2", "@types/react-redux": "^7.1.7", "@types/react-test-renderer": "16.9.2", "@types/redux-mock-store": "^1.0.2", + "@types/shelljs": "^0.8.9", "@types/sjcl": "^1.0.29", "@typescript-eslint/eslint-plugin": "^2.27.0", "@typescript-eslint/parser": "^2.27.0", @@ -123,14 +132,19 @@ "cz-conventional-changelog": "^3.1.0", "eslint": "^6.5.1", "husky": "^4.2.5", + "inquirer": "^8.1.2", "jest": "^24.9.0", "metro-minify-terser": "^0.65.1", "metro-react-native-babel-preset": "^0.65.1", + "minimist": "^1.2.5", + "node-fetch": "^2.6.1", "prettier": "^2.0.4", "react-devtools": "^4.6.0", "react-test-renderer": "16.13.1", "redux-devtools-extension": "^2.13.8", "redux-mock-store": "^1.5.4", + "shelljs": "^0.8.4", + "ts-node": "^10.1.0", "typescript": "^4.2.3" }, "jest": { @@ -146,7 +160,7 @@ }, "husky": { "hooks": { - "pre-commit": "node bin/precommit.js", + "pre-commit": "ts-node bin/precommit.ts ", "prepare-commit-msg": "exec < /dev/tty && git cz --hook || true" } }, diff --git a/src/RootNavigation.tsx b/src/RootNavigation.tsx index 2f7963eb2..b33d722ce 100644 --- a/src/RootNavigation.tsx +++ b/src/RootNavigation.tsx @@ -25,6 +25,7 @@ export type RootStackParamList = { [ScreenNames.DragToConfirm]: { title: string cancelText: string + instructionText: string onComplete: () => Promise } [ScreenNames.LoggedIn]: undefined diff --git a/src/assets/images/pinrecovery.png b/src/assets/images/pinrecovery.png index 8d77a7e0c..89b1c26c0 100644 Binary files a/src/assets/images/pinrecovery.png and b/src/assets/images/pinrecovery.png differ diff --git a/src/assets/images/pinrecovery@2x.png b/src/assets/images/pinrecovery@2x.png index cbcf9fb64..5d0aa9189 100644 Binary files a/src/assets/images/pinrecovery@2x.png and b/src/assets/images/pinrecovery@2x.png differ diff --git a/src/assets/images/pinrecovery@3x.png b/src/assets/images/pinrecovery@3x.png index 1e8735eb4..07b8a2d0e 100644 Binary files a/src/assets/images/pinrecovery@3x.png and b/src/assets/images/pinrecovery@3x.png differ diff --git a/src/assets/svg/InteractionCardOther.tsx b/src/assets/svg/InteractionCardOther.tsx index 140ffe065..85d9a448f 100644 --- a/src/assets/svg/InteractionCardOther.tsx +++ b/src/assets/svg/InteractionCardOther.tsx @@ -3,7 +3,7 @@ import { View } from 'react-native' import Svg, { Defs, Path, G, Use, Line, Rect, Polygon } from 'react-native-svg' /* SVGR has dropped some elements not supported by react-native-svg: title, filter */ -const SvgComponent: React.FC = ({ children }) => { +const InteractionCardOther: React.FC = ({ children }) => { return ( @@ -481,4 +481,4 @@ const SvgComponent: React.FC = ({ children }) => { ) } -export default SvgComponent +export default InteractionCardOther diff --git a/src/assets/svg/OtherCardMedium.tsx b/src/assets/svg/OtherCardMedium.tsx index a925a59a2..c20828b1c 100644 --- a/src/assets/svg/OtherCardMedium.tsx +++ b/src/assets/svg/OtherCardMedium.tsx @@ -3,25 +3,21 @@ import { View } from 'react-native' import Svg, { SvgProps, Path } from 'react-native-svg' /* SVGR has dropped some elements not supported by react-native-svg: title */ -const SvgComponent: React.FC = (props) => { - return ( - - - - {props.children} - - - ) -} +const SvgComponent: React.FC = (props) => ( + + + {props.children} + +) export default SvgComponent diff --git a/src/assets/svg/PurpleTickSuccess.tsx b/src/assets/svg/PurpleTickSuccess.tsx index 50269e571..dd7bcbaf4 100644 --- a/src/assets/svg/PurpleTickSuccess.tsx +++ b/src/assets/svg/PurpleTickSuccess.tsx @@ -3,7 +3,7 @@ import Svg, { G, Path } from 'react-native-svg' function PurpleTickSuccess() { return ( - + extends CarouselProps, IWithCustomStyle {} @@ -15,7 +15,7 @@ const AdoptedCarousel = ({ ...rest }: IAdoptedCarousel) => { const prevLength = usePrevious(data.length) - const carouselRef = useRef | null>(null) + const carouselRef = useRef | null>(null) const [isHidden, setIsHidden] = useState(false) @@ -30,6 +30,7 @@ const AdoptedCarousel = ({ if (data.length === 1) { setIsHidden(true) } + // eslint-disable-next-line no-unused-expressions carouselRef.current?.snapToPrev() } }, [data.length]) diff --git a/src/components/BottomBar.tsx b/src/components/BottomBar.tsx index 6910c5432..7c9eeaaa2 100644 --- a/src/components/BottomBar.tsx +++ b/src/components/BottomBar.tsx @@ -23,6 +23,7 @@ import { SCREEN_WIDTH } from '~/utils/dimensions' interface IconPropsI { label: string key: number + route: string isActive: boolean } @@ -44,12 +45,12 @@ const TABS_POSITION_BOTTOM = BP({ /* picture has invisble bottom margins, therefore adding 1 point to hide it */ const INVISIBLE_BOTTOM_MARGIN = 1 -const Tab: React.FC = ({ label, isActive }) => { - const redirectToTab = useRedirectTo(label as ScreenNames) +const Tab: React.FC = ({ label, isActive, route }) => { + const redirectToTab = useRedirectTo(route as ScreenNames) const renderIcon = () => { const color = isActive ? Colors.white : Colors.white40 - switch (label) { + switch (route) { case ScreenNames.Identity: { return } @@ -90,7 +91,9 @@ const Tab: React.FC = ({ label, isActive }) => { } const ScannerButton = () => { - const redirectToScanner = useRedirectTo(ScreenNames.Interaction) + const redirectToScanner = useRedirectTo(ScreenNames.Interaction, { + screen: ScreenNames.Scanner, + }) const insets = useSafeArea() return ( { } const BottomBar = (props: BottomTabBarProps) => { + const { descriptors } = props const { history, routeNames, routes } = props.state const getSelectedRoute = (label: string) => routes.find((el) => el.name === label) || { key: '' } @@ -154,10 +158,13 @@ const BottomBar = (props: BottomTabBarProps) => { ]} > - {routeNames.slice(0, 2).map((routeName: string, idx: number) => ( + {routes.slice(0, 2).map(({ name: routeName, key }, idx: number) => ( { ))} - {props.state.routeNames - .slice(2) - .map((routeName: string, idx: number) => ( - - ))} + {routes.slice(2).map(({ name: routeName, key }, idx: number) => ( + + ))} diff --git a/src/components/Card/Card.tsx b/src/components/Card/Card.tsx deleted file mode 100644 index c5801ef35..000000000 --- a/src/components/Card/Card.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useMemo } from 'react' -import CardHighlight from './CardHighlight' -import CardPhoto from './CardPhoto' -import DocumentHeader from './DocumentHeader' -import DocumentDots from './DocumentDots' -import OptionalFields from './OptionalFields' -import OtherHeader from './OtherHeader' -import { ICardProps, ICardComposition } from './types' -import { DocumentFields } from '~/types/credentials' -import { CardContext } from './context' - -const Card: React.FC & ICardComposition = ({ - children, - id, - type, - optionalFields, - mandatoryFields, - photo, - highlight, -}) => { - const getFieldInfo = (fieldName: string) => - mandatoryFields.find((el) => el?.label === fieldName) - - const document = getFieldInfo(DocumentFields.DocumentName) - const [restMandatoryField] = mandatoryFields.filter( - (f) => f?.label !== DocumentFields.DocumentName, - ) - - const contextValue = useMemo( - () => ({ - id, - type, - document, - restMandatoryField, - optionalFields, - photo, - highlight, - }), - [], - ) - return -} - -Card.OptionalFields = OptionalFields -Card.DocumentHeader = DocumentHeader -Card.OtherHeader = OtherHeader -Card.Highlight = CardHighlight -Card.Photo = CardPhoto -Card.Dots = DocumentDots - -export default Card diff --git a/src/components/Card/CardHighlight.tsx b/src/components/Card/CardHighlight.tsx deleted file mode 100644 index 91ac69a96..000000000 --- a/src/components/Card/CardHighlight.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react' -import { StyleSheet, View } from 'react-native' -import BP from '~/utils/breakpoints' -import { Colors } from '~/utils/colors' -import { useCard } from './context' -import { CARD_HORIZONTAL_PADDING } from './CardStyledComponents' -import { Highlight } from './Field' - -const CardHighlight: React.FC = () => { - const { highlight } = useCard() - if (!highlight) return null - return ( - - {highlight} - - ) -} - -const styles = StyleSheet.create({ - highlight: { - width: '100%', - position: 'absolute', - bottom: -1, - alignSelf: 'center', - paddingTop: BP({ default: 17, xsmall: 10 }), - paddingBottom: BP({ default: 12, xsmall: 5 }), - paddingHorizontal: CARD_HORIZONTAL_PADDING, - backgroundColor: Colors.black, - borderBottomRightRadius: 13, - borderBottomLeftRadius: 13, - zIndex: 0, - }, -}) - -export default CardHighlight diff --git a/src/components/Card/CardPhoto.tsx b/src/components/Card/CardPhoto.tsx deleted file mode 100644 index e26bd85b6..000000000 --- a/src/components/Card/CardPhoto.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' -import { Image, StyleSheet, View } from 'react-native' -import BP from '~/utils/breakpoints' -import { useCard } from './context' - -const PHOTO_SIZE = BP({ default: 82, xsmall: 60 }) - -const CardPhoto: React.FC = () => { - const { photo } = useCard() - if (!photo) return null - return ( - - - - ) -} - -const styles = StyleSheet.create({ - container: { - position: 'absolute', - right: 14, - bottom: 27, - zIndex: 10, - }, - photo: { - width: PHOTO_SIZE, - height: PHOTO_SIZE, - borderRadius: PHOTO_SIZE / 2, - zIndex: 10, - }, -}) - -export default CardPhoto diff --git a/src/components/Card/CardStyledComponents.tsx b/src/components/Card/CardStyledComponents.tsx deleted file mode 100644 index 4e01e5817..000000000 --- a/src/components/Card/CardStyledComponents.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react' -import { StyleSheet, View } from 'react-native' -import BP from '~/utils/breakpoints' -import { IWithCustomStyle } from './types' - -export const CARD_HORIZONTAL_PADDING = BP({ default: 20, xsmall: 16 }) - -export const CardContainer: React.FC<{ testID: string }> = ({ - children, - testID, -}) => - -export const CardBody: React.FC = ({ - children, - customStyles, -}) => - -const styles = StyleSheet.create({ - container: { - width: '100%', - marginBottom: 24, - }, - bodyContainer: { - height: '100%', - alignSelf: 'center', - alignItems: 'flex-start', - paddingHorizontal: CARD_HORIZONTAL_PADDING, - }, -}) diff --git a/src/components/Card/DocumentCard.tsx b/src/components/Card/DocumentCard.tsx deleted file mode 100644 index 3d7a0f88c..000000000 --- a/src/components/Card/DocumentCard.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import DocumentCardMedium from '~/assets/svg/DocumentCardMedium' -import BP from '~/utils/breakpoints' -import Card from './Card' -import { - CardBody, - CardContainer, - CARD_HORIZONTAL_PADDING, -} from './CardStyledComponents' -import { ICardProps } from './types' - -const DocumentCard: React.FC = ({ - id, - type, - mandatoryFields, - optionalFields, - photo, - highlight, -}) => { - return ( - - - - - - - - - - - - - - - ) -} - -export default DocumentCard diff --git a/src/components/Card/DocumentDots.tsx b/src/components/Card/DocumentDots.tsx deleted file mode 100644 index 6a2099ce8..000000000 --- a/src/components/Card/DocumentDots.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React, { useMemo } from 'react' - -import { useRedirectTo } from '~/hooks/navigation' -import { useToasts } from '~/hooks/toasts' -import { strings } from '~/translations' -import { ScreenNames } from '~/types/screens' -import { Colors } from '~/utils/colors' -import { useCard } from './context' -import { IWithCustomStyle } from './types' -import Dots from '../Dots' -import { useDeleteCredential } from '~/hooks/credentials' - -const DocumentDots: React.FC = ({ customStyles }) => { - const { scheduleWarning } = useToasts() - const redirectToContactUs = useRedirectTo(ScreenNames.ContactUs) - const deleteCredential = useDeleteCredential() - - const { id, photo, document, restMandatoryField, optionalFields } = useCard() - - const mandatoryFields = restMandatoryField ? [restMandatoryField] : [] - const claimsDisplay = [...mandatoryFields, ...optionalFields] - const title = document?.value as string - - const deleteTitle = `${strings.DO_YOU_WANT_TO_DELETE} ${title}?` - const cancelText = strings.CANCEL - const handleDelete = async () => { - try { - await deleteCredential(id) - } catch (e) { - scheduleWarning({ - title: strings.WHOOPS, - message: strings.ERROR_TOAST_MSG, - interact: { - label: strings.REPORT, - onInteract: redirectToContactUs, // TODO: change to Reporting screen once available - }, - }) - } - } - - const popupOptions = useMemo( - () => [ - { - title: strings.INFO, - navigation: { - screen: ScreenNames.CredentialDetails, - params: { - fields: claimsDisplay, - photo, - title, - }, - }, - }, - { - title: strings.DELETE, - navigation: { - screen: ScreenNames.DragToConfirm, - params: { - title: deleteTitle, - cancelText, - onComplete: handleDelete, - }, - }, - }, - ], - [], - ) - - return ( - - ) -} - -export default DocumentDots diff --git a/src/components/Card/DocumentHeader.tsx b/src/components/Card/DocumentHeader.tsx deleted file mode 100644 index a2a612708..000000000 --- a/src/components/Card/DocumentHeader.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { useState } from 'react' -import { StyleSheet, View } from 'react-native' -import BP from '~/utils/breakpoints' -import { useCard } from './context' -import { SpecialField, TitleField, TextLayoutEvent } from './Field' -import Space from '~/components/Space' - -const DocumentHeader: React.FC = () => { - const { document, restMandatoryField } = useCard() - const [isHeaderScalled, setIsHeaderScaled] = useState(false) - - const handleHeaderTextLayout = (e: TextLayoutEvent) => { - if (!isHeaderScalled) { - setIsHeaderScaled(e.nativeEvent.lines.length > 2) - } - } - - return ( - <> - - - {document?.value} - - - - {restMandatoryField && ( - <> - - - {restMandatoryField?.value} - - - - )} - - ) -} - -const styles = StyleSheet.create({ - header: { - flexDirection: 'row', - justifyContent: 'space-between', - }, - scaledDocumentField: { - fontSize: BP({ default: 22, xsmall: 20 }), - lineHeight: 22, - marginBottom: 20, - }, -}) - -export default DocumentHeader diff --git a/src/components/Card/Field.tsx b/src/components/Card/Field.tsx deleted file mode 100644 index 7123e2dd8..000000000 --- a/src/components/Card/Field.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, { SyntheticEvent } from 'react' -import { StyleProp, StyleSheet, Text, TextProps, TextStyle } from 'react-native' -import BP from '~/utils/breakpoints' -import { Colors } from '~/utils/colors' -import { Fonts } from '~/utils/fonts' - -//FIXME: when @TextLayoutEvent type is added to RN, should replace this line -export type TextLayoutEvent = SyntheticEvent<{}, { lines: Array }> - -interface IFieldProps { - customStyles?: StyleProp - onTextLayout?: (e: TextLayoutEvent) => void -} - -const Field: React.FC = (props) => { - const { children, customStyles, ...rest } = props - return ( - - {children} - - ) -} - -export const TitleField: React.FC = (props) => ( - -) - -export const SpecialField: React.FC = (props) => ( - -) - -export const FieldName: React.FC = (props) => ( - -) - -export const FieldValue: React.FC = (props) => ( - -) - -export const Highlight: React.FC = (props) => ( - -) - -const styles = StyleSheet.create({ - text: { - textAlign: 'left', - fontFamily: Fonts.Regular, - color: Colors.black90, - }, - fieldName: { - fontSize: 16, - lineHeight: 16, - color: Colors.slateGray, - }, - fieldValue: { - fontSize: BP({ default: 20, xsmall: 18 }), - lineHeight: 20, - color: Colors.black, - fontFamily: Fonts.Medium, - }, - specialField: { - color: Colors.black, - fontFamily: Fonts.Medium, - marginLeft: 10, - fontSize: 20, - lineHeight: 20, - }, - titleField: { - fontSize: BP({ default: 28, xsmall: 24 }), - lineHeight: BP({ default: 28, xsmall: 24 }), - marginBottom: 10, - }, - highlight: { - fontSize: 26, - color: Colors.white90, - }, -}) diff --git a/src/components/Card/OptionalFields.tsx b/src/components/Card/OptionalFields.tsx deleted file mode 100644 index 80a686fcc..000000000 --- a/src/components/Card/OptionalFields.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import React, { useRef, useState } from 'react' -import { StyleSheet, View } from 'react-native' -import BP from '~/utils/breakpoints' -import { useCard } from './context' -import { FieldName, FieldValue, TextLayoutEvent } from './Field' -import { ICardComposition } from './types' -import { CredentialCategories } from '~/types/credentials' -import Space from '~/components/Space' - -const OptionalFields: ICardComposition['OptionalFields'] = ({ - customStyles: customContainerStyles, - lastFieldPadding = '30%', -}) => { - const { optionalFields, highlight, photo } = useCard() - const [displayedOptionalFields, setDisplayedOptionalFields] = useState( - optionalFields.slice(0, 3), - ) - - const lines = useRef(0) - - const handleOptionalFieldTextLayout = () => { - let calculatedTimes = 0 - return (e: TextLayoutEvent) => { - calculatedTimes++ - // disable lines manipulation if the number of times this function was invoked - // exceeds length of optional fields twice (because we calculate field name and - // field value ) - if (calculatedTimes < optionalFields.length * 2 + 1) { - const numberOfLines = e.nativeEvent.lines.length - lines.current += numberOfLines - if (calculatedTimes === optionalFields.length * 2) { - /* check wether to show last optional field */ - if ( - lines.current > 7 && - (highlight || (photo && CredentialCategories.document)) - ) { - setDisplayedOptionalFields((prevState) => - prevState.slice( - 0, - Math.floor(lines.current / optionalFields.length), - ), - ) - } else if (lines.current > 9 && !highlight) { - setDisplayedOptionalFields((prevState) => prevState.slice(0, 3)) - } - } - } - } - } - - const onTextLayoutChange = handleOptionalFieldTextLayout() - - const renderFieldValue = ( - value: string | number, - padding?: string | number, - ) => { - return ( - - {value} - - ) - } - - const renderFieldName = (value: string) => { - return ( - - {value}: - - ) - } - - return ( - - {displayedOptionalFields.map((pField, idx) => ( - - {renderFieldName(pField.label)} - {/* in case thers is a photo we should display last field differently */} - {idx === displayedOptionalFields.length - 1 && photo - ? renderFieldValue(pField.value, lastFieldPadding) - : renderFieldValue(pField.value)} - - - ))} - - ) -} - -const styles = StyleSheet.create({ - container: { - alignItems: 'flex-start', - marginTop: 10, - width: '100%', - }, -}) - -export default OptionalFields diff --git a/src/components/Card/OtherCard.tsx b/src/components/Card/OtherCard.tsx deleted file mode 100644 index 6e1cb792f..000000000 --- a/src/components/Card/OtherCard.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react' -import { View } from 'react-native' -import OtherCardMedium from '~/assets/svg/OtherCardMedium' -import BP from '~/utils/breakpoints' -import Card from './Card' -import { - CardBody, - CardContainer, - CARD_HORIZONTAL_PADDING, -} from './CardStyledComponents' -import { ICardProps } from './types' - -const OtherCard: React.FC = ({ - id, - type, - mandatoryFields, - optionalFields, - photo, -}) => { - return ( - - - - - - - - - - - - - - - - - - - ) -} - -export default OtherCard diff --git a/src/components/Card/OtherHeader.tsx b/src/components/Card/OtherHeader.tsx deleted file mode 100644 index e963da0a6..000000000 --- a/src/components/Card/OtherHeader.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useState } from 'react' -import { Image, StyleSheet, View } from 'react-native' -import { getCredentialUIType } from '~/hooks/signedCredentials/utils' -import { strings } from '~/translations' -import BP from '~/utils/breakpoints' -import Space from '../Space' -import { useCard } from './context' -import { FieldName, TitleField, TextLayoutEvent } from './Field' - -const LARGE_LOGO_SIZE = BP({ default: 78, xsmall: 60 }) -const SMALL_LOGO_SIZE = 37 - -const OtherHeader: React.FC = () => { - const { document, photo: logo, type } = useCard() - const [isHeaderScalled, setIsHeaderScaled] = useState(false) - const credentialUIType = getCredentialUIType(type) - - const handleHeaderTextLayout = (e: TextLayoutEvent) => { - if (!isHeaderScalled) { - setIsHeaderScaled(e.nativeEvent.lines.length > 2) - } - } - - return ( - - - - {credentialUIType} - - - - - {document?.value} - - - - {logo ? ( - - - - ) : null} - - ) -} - -const styles = StyleSheet.create({ - header: { - flexDirection: 'row', - width: '100%', - }, - scaledDocumentField: { - fontSize: 22, - lineHeight: 22, - marginBottom: 20, - }, -}) - -export default OtherHeader diff --git a/src/components/Card/context.ts b/src/components/Card/context.ts deleted file mode 100644 index e59a592fe..000000000 --- a/src/components/Card/context.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createContext } from 'react' -import { ICardProps, IField } from './types' -import { useCustomContext } from '~/hooks/context' - -interface ICardContext extends Omit { - document: IField | undefined | null - restMandatoryField: IField | undefined | null -} - -export const CardContext = createContext(undefined) -CardContext.displayName = 'CardContext' - -export const useCard = useCustomContext(CardContext) diff --git a/src/components/Card/types.ts b/src/components/Card/types.ts deleted file mode 100644 index cc93f4e71..000000000 --- a/src/components/Card/types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { StyleProp, ViewStyle } from 'react-native' -import { ClaimEntry } from 'jolocom-lib/js/credentials/credential/types' - -export interface IWithCustomStyle { - customStyles?: StyleProp -} - -export interface IField { - label: string - value: ClaimEntry -} - -export interface ICardProps { - id: string - type: string - optionalFields: IField[] - mandatoryFields: Array - photo?: string | undefined - highlight?: string | undefined -} - -interface IOptionalFields extends IWithCustomStyle { - lastFieldPadding?: string -} - -export interface ICardComposition { - OptionalFields: React.FC - DocumentHeader: React.FC - OtherHeader: React.FC - Highlight: React.FC - Photo: React.FC - Dots: React.FC -} diff --git a/src/components/Cards/DocumentSectionCards/DocumentSectionDocumentCard.tsx b/src/components/Cards/DocumentSectionCards/DocumentSectionDocumentCard.tsx new file mode 100644 index 000000000..669304562 --- /dev/null +++ b/src/components/Cards/DocumentSectionCards/DocumentSectionDocumentCard.tsx @@ -0,0 +1,239 @@ +import React from 'react' +import { View, Image, StyleSheet } from 'react-native' + +import DocumentCardMedium from '~/assets/svg/DocumentCardMedium' +import ScaledCard, { ScaledText, ScaledView } from '../ScaledCard' +import { useCredentialNameScale, usePruneFields } from '../hooks' +import { Colors } from '~/utils/colors' +import { Fonts } from '~/utils/fonts' +import { + ORIGINAL_DOCUMENT_CARD_HEIGHT, + ORIGINAL_DOCUMENT_CARD_WIDTH, + ORIGINAL_DOCUMENT_SCREEN_WIDTH, +} from './consts' +import { CardMoreBtn } from './components' +import { DocumentCardProps } from './types' +import { FieldsCalculator } from '../InteractionShare/components' + +const MAX_FIELDS = 3 +const MAX_FIELD_LINES = 5 + +const DocumentSectionDocumentCard: React.FC = ({ + credentialName, + holderName, + fields, + photo, + highlight, + onHandleMore, +}) => { + const { isCredentialNameScaled, handleCredentialNameTextLayout } = + useCredentialNameScale() + + const { + displayedFields, + handleFieldValueLayout, + handleFieldValuesVisibility, + } = usePruneFields(fields, MAX_FIELDS, MAX_FIELD_LINES) + + return ( + + + + + + {credentialName} + + + + + + {holderName} + + + + + {displayedFields.map((f, idx) => ( + <> + {idx !== 0 && } + + {f.label.trim()}: + + + + handleFieldValueLayout(e, idx) + } + scaleStyle={styles.fieldText} + style={[ + styles.mediumText, + { + width: + photo && idx === displayedFields.length - 1 + ? '66.4%' + : '100%', + }, + ]} + > + {f.value} + + + ))} + + + + {photo && ( + + + + )} + {highlight && ( + + + {highlight.toUpperCase()} + + + )} + {/* Dots - more action */} + + + ) +} + +const styles = StyleSheet.create({ + bodyContainer: { + paddingTop: 22, + paddingHorizontal: 14, + paddingBottom: 14, + }, + credentialName: { + fontSize: 28, + lineHeight: 28, + }, + credentialNameScaled: { + fontSize: 22, + lineHeight: 22, + }, + holderName: { + fontSize: 20, + lineHeight: 20, + }, + photoContainer: { + position: 'absolute', + overflow: 'hidden', + zIndex: 10, + }, + photoContainerScaled: { + bottom: 27, + right: 14, + width: 82, + height: 82, + borderRadius: 41, + }, + photo: { + width: '100%', + height: '100%', + }, + highlightContainerScaled: { + bottom: 0, + height: 56, + paddingTop: 17, + paddingBottom: 13, + paddingHorizontal: 23, + }, + highlightContainer: { + position: 'absolute', + width: '100%', + backgroundColor: Colors.black, + zIndex: 9, + }, + highlight: { + fontSize: 26, + color: Colors.white90, + }, + regularText: { + fontFamily: Fonts.Regular, + color: Colors.black, + }, + mediumText: { + fontFamily: Fonts.Medium, + color: Colors.black, + }, + fieldLabel: { + fontSize: 16, + lineHeight: 16, + color: Colors.slateGray, + }, + fieldText: { + fontSize: 20, + lineHeight: 20, + letterSpacing: 0.14, + }, +}) + +export default DocumentSectionDocumentCard diff --git a/src/components/Cards/DocumentSectionCards/DocumentSectionOtherCard.tsx b/src/components/Cards/DocumentSectionCards/DocumentSectionOtherCard.tsx new file mode 100644 index 000000000..47ca7dd5e --- /dev/null +++ b/src/components/Cards/DocumentSectionCards/DocumentSectionOtherCard.tsx @@ -0,0 +1,203 @@ +import React, { useMemo } from 'react' +import { View, Image, StyleSheet } from 'react-native' +import OtherCardMedium from '~/assets/svg/OtherCardMedium' +import { Fonts } from '~/utils/fonts' +import { Colors } from '~/utils/colors' +import { getCredentialUIType } from '~/hooks/signedCredentials/utils' +import { useCredentialNameScale, useTrimFields } from '../hooks' +import ScaledCard, { ScaledText, ScaledView } from '../ScaledCard' +import { + ORIGINAL_DOCUMENT_CARD_HEIGHT, + ORIGINAL_DOCUMENT_CARD_WIDTH, + ORIGINAL_DOCUMENT_SCREEN_WIDTH, +} from './consts' +import { CardMoreBtn } from './components' +import { OtherCardProps } from './types' + +const DocumentSectionOtherCard: React.FC = ({ + credentialType, + credentialName, + fields, + logo, + onHandleMore, +}) => { + const { isCredentialNameScaled, handleCredentialNameTextLayout } = + useCredentialNameScale() + + const { displayedFields, onTextLayoutChange } = useTrimFields(fields, logo) + + return ( + + + + + + {credentialType} + + + + {credentialName} + + + + {displayedFields.map((f, idx) => ( + <> + + {/* TODO: share the same with document card */} + + {f.label}: + + + + {f.value.trim()} + + + ))} + + + + {logo && ( + + + + )} + {/* Dots - more action */} + + + ) +} + +const styles = StyleSheet.create({ + otherBodyContainer: { + paddingVertical: 16, + paddingRight: 22, + paddingLeft: 20, + height: '100%', + }, + otherCredentialType: { + fontSize: 18, + lineHeight: 18, + letterSpacing: 0.09, + color: Colors.jumbo, + }, + otherCredentialName: { + fontSize: 28, + lineHeight: 28, + }, + otherCredentialNameScaled: { + fontSize: 22, + lineHeight: 24, + }, + logoContainerDefault: { + overflow: 'hidden', + position: 'absolute', + zIndex: 10, + borderRadius: 40, + }, + otherLogoContainer: { + top: 16, + right: 16, + width: 78, + height: 78, + }, + otherLogoContainerScaled: { + top: 14, + right: 19, + width: 37, + height: 37, + }, + image: { + width: '100%', + height: '100%', + }, + regularText: { + fontFamily: Fonts.Regular, + color: Colors.black, + }, + mediumText: { + fontFamily: Fonts.Medium, + color: Colors.black, + }, + fieldLabel: { + fontSize: 16, + lineHeight: 16, + color: Colors.slateGray, + }, + fieldText: { + fontSize: 20, + lineHeight: 20, + letterSpacing: 0.14, + }, +}) + +export default DocumentSectionOtherCard diff --git a/src/components/Cards/DocumentSectionCards/components.tsx b/src/components/Cards/DocumentSectionCards/components.tsx new file mode 100644 index 000000000..7d0441cc9 --- /dev/null +++ b/src/components/Cards/DocumentSectionCards/components.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { StyleSheet, TouchableOpacity, ViewStyle } from 'react-native' + +import { Colors } from '~/utils/colors' +import { ScaledView } from '../ScaledCard' + +interface Props { + onPress: () => void + positionStyles: Partial> +} + +export const CardMoreBtn: React.FC = ({ onPress, positionStyles }) => ( + + + {[...Array(3).keys()].map((c) => ( + + ))} + + +) + +const styles = StyleSheet.create({ + dotsContainerScaled: { + paddingHorizontal: 3, + }, + dotsContainer: { + position: 'absolute', + zIndex: 100, + }, + dotsBtn: { + alignItems: 'center', + justifyContent: 'center', + flexDirection: 'row', + paddingVertical: 10, + }, + dot: { + width: 4, + height: 4, + borderRadius: 2, + marginHorizontal: 2, + backgroundColor: Colors.black, + }, +}) diff --git a/src/components/Cards/DocumentSectionCards/consts.ts b/src/components/Cards/DocumentSectionCards/consts.ts new file mode 100644 index 000000000..de421495d --- /dev/null +++ b/src/components/Cards/DocumentSectionCards/consts.ts @@ -0,0 +1,3 @@ +export const ORIGINAL_DOCUMENT_CARD_WIDTH = 320 +export const ORIGINAL_DOCUMENT_CARD_HEIGHT = 398 +export const ORIGINAL_DOCUMENT_SCREEN_WIDTH = 375 diff --git a/src/components/Cards/DocumentSectionCards/index.ts b/src/components/Cards/DocumentSectionCards/index.ts new file mode 100644 index 000000000..e38791bf9 --- /dev/null +++ b/src/components/Cards/DocumentSectionCards/index.ts @@ -0,0 +1,2 @@ +export { default as DocumentCard } from './DocumentSectionDocumentCard' +export { default as OtherCard } from './DocumentSectionOtherCard' diff --git a/src/components/Cards/DocumentSectionCards/types.ts b/src/components/Cards/DocumentSectionCards/types.ts new file mode 100644 index 000000000..658e34a26 --- /dev/null +++ b/src/components/Cards/DocumentSectionCards/types.ts @@ -0,0 +1,18 @@ +import { DisplayVal } from '@jolocom/sdk/js/credentials' + +interface CommonDocumentsProps { + credentialName: string + fields: Array> + onHandleMore: () => void +} + +export interface OtherCardProps extends CommonDocumentsProps { + credentialType: string + logo?: string +} + +export interface DocumentCardProps extends CommonDocumentsProps { + holderName: string + highlight?: string + photo?: string +} diff --git a/src/components/Cards/InteractionOffer/InteractionOfferCard.tsx b/src/components/Cards/InteractionOffer/InteractionOfferCard.tsx new file mode 100644 index 000000000..738584194 --- /dev/null +++ b/src/components/Cards/InteractionOffer/InteractionOfferCard.tsx @@ -0,0 +1,106 @@ +import React from 'react' +import { StyleSheet } from 'react-native' + +import InteractionCardDoc from '~/assets/svg/InteractionCardDoc' +import InteractionCardOther from '~/assets/svg/InteractionCardOther' +import useTranslation from '~/hooks/useTranslation' +import { Colors } from '~/utils/colors' +import { commonStyles } from '../commonStyles' +import ScaledCard, { ScaledText, ScaledView } from '../ScaledCard' +import { + ORIGINAL_DOCUMENT_OFFER_CARD_HEIGHT, + ORIGINAL_DOCUMENT_OFFER_CARD_WIDTH, +} from './consts' +import { CardType, InteractionOfferCardProps } from './types' + +export const InteractionOfferCard: React.FC< + InteractionOfferCardProps & CardType +> = ({ cardType, credentialName, fields }) => { + const { t } = useTranslation() + const Card = + cardType === 'document' ? InteractionCardDoc : InteractionCardOther + return ( + + + + + {credentialName} + + + + {t('CredentialOffer.cardInfoHeader')}: + + + {fields.length ? ( + fields.slice(0, 3).map((f, idx) => ( + <> + {idx !== 0 && } + + {f.label} + + + + + )) + ) : ( + + {t('CredentialOffer.cardNoPreview')} + + )} + + + + ) +} + +const styles = StyleSheet.create({ + fieldSectionTitle: { + fontSize: 16, + }, + credentialName: { + fontSize: 22, + lineHeight: 24, + letterSpacing: 0.15, + color: Colors.black80, + }, + bodyContainer: { + paddingBottom: 21, + paddingTop: 16, + paddingHorizontal: 14, + }, + valuePlaceholder: { + width: '45.8%', + height: 20, + borderRadius: 5, + backgroundColor: Colors.alto, + }, + preview: { + textAlign: 'left', + }, +}) diff --git a/src/components/Cards/InteractionOffer/consts.ts b/src/components/Cards/InteractionOffer/consts.ts new file mode 100644 index 000000000..7b331beb9 --- /dev/null +++ b/src/components/Cards/InteractionOffer/consts.ts @@ -0,0 +1,2 @@ +export const ORIGINAL_DOCUMENT_OFFER_CARD_WIDTH = 368 +export const ORIGINAL_DOCUMENT_OFFER_CARD_HEIGHT = 232 diff --git a/src/components/Cards/InteractionOffer/index.tsx b/src/components/Cards/InteractionOffer/index.tsx new file mode 100644 index 000000000..0635e89fe --- /dev/null +++ b/src/components/Cards/InteractionOffer/index.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { InteractionOfferCard } from './InteractionOfferCard' +import { InteractionOfferCardProps } from './types' + +export const InteractionOfferDocumentCard: React.FC = + (props) => +export const InteractionOfferOtherCard: React.FC = ( + props, +) => diff --git a/src/components/Cards/InteractionOffer/types.ts b/src/components/Cards/InteractionOffer/types.ts new file mode 100644 index 000000000..2f91bd016 --- /dev/null +++ b/src/components/Cards/InteractionOffer/types.ts @@ -0,0 +1,9 @@ +import { DisplayVal } from '@jolocom/sdk/js/credentials' + +export type InteractionOfferCardProps = { + credentialName: string + fields: Array>> +} +export type CardType = { + cardType: 'document' | 'other' +} diff --git a/src/components/Cards/InteractionShare/InteractionShareDocumentCard.tsx b/src/components/Cards/InteractionShare/InteractionShareDocumentCard.tsx new file mode 100644 index 000000000..d0bd3b0d7 --- /dev/null +++ b/src/components/Cards/InteractionShare/InteractionShareDocumentCard.tsx @@ -0,0 +1,204 @@ +import React, { useState } from 'react' +import { Image, StyleSheet, View } from 'react-native' + +import InteractionCardDoc from '~/assets/svg/InteractionCardDoc' +import { TextLayoutEvent } from '~/types/props' +import { Colors } from '~/utils/colors' +import { commonStyles } from '../commonStyles' +import { useCalculateFieldLines } from '../hooks' +import ScaledCard, { ScaledText, ScaledView } from '../ScaledCard' + +import { FieldsCalculator, SelectedToggle } from './components' +import { + MAX_FIELD_DOC, + ORIGINAL_DOCUMENT_SHARE_CARD_HEIGHT, + ORIGINAL_DOCUMENT_SHARE_CARD_WIDTH, +} from './consts' +import { shareStyles } from './styles' +import { InteractionShareDocumentCardProps } from './types' + +export const InteractionShareDocumentCard: React.FC = + ({ credentialName, holderName, fields, photo, highlight, selected }) => { + /** + * Logic to calculate number of lines a holder name takes + * to decide how many fields can be displayed + */ + const [holderNameLines, setHolderNameLines] = useState(0) + const handleHolderNameLayout = (e: TextLayoutEvent) => { + const lines = e.nativeEvent.lines.length + setHolderNameLines(lines) + } + + const { fieldLines, handleFieldValueLayout } = useCalculateFieldLines() + + const handleFieldValuesVisibility = ( + child: React.ReactNode, + idx: number, + ) => { + if (idx + 1 > MAX_FIELD_DOC) { + /* 1. Do not display anything that is more than max */ + return null + } else if ( + (!!highlight && idx > 0 && fieldLines[0] > 1) || + (!!highlight && idx > 0 && holderNameLines > 1) + ) { + /** + * 2. Do not display all the fields besides first if number of + * lines of the first field is more than 1 and there is a highlight + */ + return null + } + return child + } + + const handleNumberOfValueLinesToDisplay = (idx: number) => + idx !== 0 ? (fieldLines[0] > 1 || !!highlight ? 1 : 2) : 2 + + return ( + + + + + {credentialName} + + + handleHolderNameLayout(e)} + scaleStyle={styles.documentHolderName} + style={commonStyles.mediumText} + > + {holderName} + + {/* TODO: this doesn't include logic when padding is bigger */} + + + {fields.map((f, idx) => ( + + + {idx !== 0 && ( + + )} + + {f.label}: + + + + handleFieldValueLayout(e, idx) + } + scaleStyle={shareStyles.fieldValue} + style={commonStyles.regularText} + > + {f.value} + + + + ))} + + + + {highlight && ( + + + {highlight.toUpperCase()} + + + )} + {photo && ( + + + + )} + {typeof selected !== 'undefined' && ( + + )} + + ) + } + +const styles = StyleSheet.create({ + documentBodyContainer: { + paddingBottom: 18, + paddingTop: 16, + paddingLeft: 20, + paddingRight: 17, + }, + documentCredentialName: { + width: '90%', + fontSize: 22, + lineHeight: 24, + letterSpacing: 0.15, + color: Colors.black80, + }, + documentHolderName: { + fontSize: 28, + lineHeight: 28, + }, + documentPhotoContainer: { + position: 'absolute', + overflow: 'hidden', + bottom: 18, + right: 17, + borderRadius: 70, + width: 105, + height: 105, + }, + documentPhoto: { + width: '100%', + height: '100%', + }, + documentHighlightContainer: { + position: 'absolute', + bottom: 0, + width: '100%', + backgroundColor: 'black', + zIndex: 9, + height: 56, + paddingTop: 17, + paddingBottom: 13, + paddingHorizontal: 23, + }, + documentHighlight: { + color: Colors.white, + fontSize: 26, + }, +}) diff --git a/src/components/Cards/InteractionShare/InteractionShareOtherCard.tsx b/src/components/Cards/InteractionShare/InteractionShareOtherCard.tsx new file mode 100644 index 000000000..feea980c0 --- /dev/null +++ b/src/components/Cards/InteractionShare/InteractionShareOtherCard.tsx @@ -0,0 +1,90 @@ +import React from 'react' +import { StyleSheet } from 'react-native' +import InteractionCardOther from '~/assets/svg/InteractionCardOther' + +import { Colors } from '~/utils/colors' +import { commonStyles } from '../commonStyles' +import { usePruneFields } from '../hooks' +import ScaledCard, { ScaledText, ScaledView } from '../ScaledCard' +import { FieldsCalculator, SelectedToggle } from './components' +import { shareStyles } from './styles' +import { InteractionShareOtherCardProps } from './types' + +const MAX_FIELDS = 3 +const MAX_FIELD_LINES = 3 + +export const InteractionShareOtherCard: React.FC = + ({ credentialName, fields, selected }) => { + const { + displayedFields, + handleFieldValueLayout, + handleFieldValuesVisibility, + } = usePruneFields(fields, MAX_FIELDS, MAX_FIELD_LINES) + + return ( + + + + + {credentialName} + + + + {displayedFields.map((f, idx) => ( + <> + {idx !== 0 && ( + + )} + + + {f.label}: + + + {/* TODO: the same as in document card */} + + handleFieldValueLayout(e, idx) + } + scaleStyle={shareStyles.fieldValue} + style={commonStyles.regularText} + > + {f.value} + + + ))} + + + + {typeof selected !== 'undefined' && ( + + )} + + ) + } + +const styles = StyleSheet.create({ + otherBodyContainer: { + // TODO: correct values once available + // add width once available + paddingBottom: 18, + paddingTop: 16, + paddingLeft: 20, + paddingRight: 17, + width: '73%', + }, + otherCredentialName: { + color: Colors.black80, + fontSize: 22, + lineHeight: 24, + }, +}) diff --git a/src/components/Cards/InteractionShare/components.tsx b/src/components/Cards/InteractionShare/components.tsx new file mode 100644 index 000000000..cd48f01c5 --- /dev/null +++ b/src/components/Cards/InteractionShare/components.tsx @@ -0,0 +1,51 @@ +import React from 'react' +import { StyleSheet } from 'react-native' +import { PurpleTickSuccess } from '~/assets/svg' +import { Colors } from '~/utils/colors' +import { ScaledView } from '../ScaledCard' +import { FieldsCalculatorProps } from './types' + +export const FieldsCalculator: React.FC<{ + cbFieldsVisibility: FieldsCalculatorProps +}> = ({ children, cbFieldsVisibility }) => + React.Children.map(children, cbFieldsVisibility) as React.ReactElement< + unknown, + string + > | null + +export const SelectedToggle: React.FC<{ selected: boolean }> = ({ + selected, +}) => { + return ( + + {selected ? ( + + ) : ( + + )} + + ) +} + +const styles = StyleSheet.create({ + selectIndicator: { + position: 'absolute', + width: 20, + height: 20, + top: 8, + right: 8, + }, + notSelected: { + borderColor: Colors.black, + opacity: 0.3, + }, + notSelectedScale: { + width: 20, + height: 20, + borderWidth: 1, + borderRadius: 10, + }, +}) diff --git a/src/components/Cards/InteractionShare/consts.ts b/src/components/Cards/InteractionShare/consts.ts new file mode 100644 index 000000000..cc343195b --- /dev/null +++ b/src/components/Cards/InteractionShare/consts.ts @@ -0,0 +1,3 @@ +export const ORIGINAL_DOCUMENT_SHARE_CARD_WIDTH = 368 +export const ORIGINAL_DOCUMENT_SHARE_CARD_HEIGHT = 232 +export const MAX_FIELD_DOC = 2 diff --git a/src/components/Cards/InteractionShare/index.ts b/src/components/Cards/InteractionShare/index.ts new file mode 100644 index 000000000..7e15d56be --- /dev/null +++ b/src/components/Cards/InteractionShare/index.ts @@ -0,0 +1,2 @@ +export { InteractionShareDocumentCard } from './InteractionShareDocumentCard' +export { InteractionShareOtherCard } from './InteractionShareOtherCard' diff --git a/src/components/Cards/InteractionShare/styles.ts b/src/components/Cards/InteractionShare/styles.ts new file mode 100644 index 000000000..855f5e9fb --- /dev/null +++ b/src/components/Cards/InteractionShare/styles.ts @@ -0,0 +1,9 @@ +import { StyleSheet } from 'react-native' + +export const shareStyles = StyleSheet.create({ + fieldValue: { + fontSize: 18, + lineHeight: 18, + letterSpacing: 0.09, + }, +}) diff --git a/src/components/Cards/InteractionShare/types.ts b/src/components/Cards/InteractionShare/types.ts new file mode 100644 index 000000000..5ce7f3052 --- /dev/null +++ b/src/components/Cards/InteractionShare/types.ts @@ -0,0 +1,18 @@ +import { ReactNode } from 'react' +import { DisplayVal } from '@jolocom/sdk/js/credentials' + +export type FieldsCalculatorProps = (child: ReactNode, idx: number) => ReactNode + +type TCommonCardProps = { + credentialName: string + fields: Array> + selected?: boolean +} + +export type InteractionShareDocumentCardProps = { + holderName: string + highlight?: string + photo?: string +} & TCommonCardProps + +export type InteractionShareOtherCardProps = TCommonCardProps diff --git a/src/components/Cards/ScaledCard/context.ts b/src/components/Cards/ScaledCard/context.ts new file mode 100644 index 000000000..4057fc170 --- /dev/null +++ b/src/components/Cards/ScaledCard/context.ts @@ -0,0 +1,9 @@ +import { createContext } from 'react' + +import { IScaledCardContext } from './types' +import { useCustomContext } from '~/hooks/context' + +export const ScaledCardContext = + createContext(undefined) + +export const useScaledCard = useCustomContext(ScaledCardContext) diff --git a/src/components/Cards/ScaledCard/index.tsx b/src/components/Cards/ScaledCard/index.tsx new file mode 100644 index 000000000..5507210ba --- /dev/null +++ b/src/components/Cards/ScaledCard/index.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react' +import { LayoutChangeEvent, LayoutRectangle, Text, View } from 'react-native' +import { getCardDimensions } from '../getCardDimenstions' +import { ScaledCardContext, useScaledCard } from './context' +import { + IScaledCardProps, + IScaledComponentProps, + TSupportedComponentProps, +} from './types' +import { handleContainerLayout, scaleStyleObject } from './utils' + +const ScaledCard: React.FC = ({ + originalHeight, + originalWidth, + scaleToFit = false, + originalScreenWidth, + style: viewStyle = {}, + scaleStyle = {}, + children, + ...viewProps +}) => { + const [containerDimensions, setContainerDimensions] = + useState>() + + const isContainerLayoutReady = scaleToFit && !containerDimensions + + const { scaleBy, scaledHeight, scaledWidth } = getCardDimensions( + originalWidth, + originalHeight, + { + ...(scaleToFit + ? { containerWidth: containerDimensions?.width! } + : { originalScreenWidth: originalScreenWidth! }), + }, + ) + + const handleLayout = (e: LayoutChangeEvent) => { + const { width, height } = handleContainerLayout(e) + setContainerDimensions({ + height, + width, + }) + } + + return ( + + + {children} + + + ) +} + +/* + * The HOC is used to inject the newly scaled styles into components that accept + * the `style` prop. + */ + +const withScaledComponent = + ( + Component: React.ElementType, + ): React.FC> => + ({ scaleStyle, style = {}, children, ...props }) => { + const { scaleBy } = useScaledCard() + const scaledStyles = scaleStyleObject(scaleStyle as T['style'], scaleBy) + return ( + // @ts-expect-error Typing the @Component as React.Element removes the error, + // but loses the type inheritance of T + + {children} + + ) + } + +export const ScaledView = withScaledComponent(View) +export const ScaledText = withScaledComponent(Text) + +export default ScaledCard diff --git a/src/components/Cards/ScaledCard/types.ts b/src/components/Cards/ScaledCard/types.ts new file mode 100644 index 000000000..54301a110 --- /dev/null +++ b/src/components/Cards/ScaledCard/types.ts @@ -0,0 +1,28 @@ +import { TextProps, ViewProps, StyleProp, ViewStyle } from 'react-native' + +export interface IScaledCardContext { + scaleBy: number +} + +interface IScaleToFitProp { + scaleToFit: boolean + originalScreenWidth?: never +} + +interface IOriginalScreenWidthProp { + scaleToFit?: never + originalScreenWidth: number +} + +export type IScaledCardProps = { + originalWidth: number + originalHeight: number + scaleStyle?: StyleProp +} & (IScaleToFitProp | IOriginalScreenWidthProp) & + ViewProps + +export type TSupportedComponentProps = ViewProps | TextProps + +export type IScaledComponentProps = { + scaleStyle: StyleProp +} & T diff --git a/src/components/Cards/ScaledCard/utils.ts b/src/components/Cards/ScaledCard/utils.ts new file mode 100644 index 000000000..e66d55730 --- /dev/null +++ b/src/components/Cards/ScaledCard/utils.ts @@ -0,0 +1,36 @@ +import { + LayoutChangeEvent, + StyleSheet, + TextStyle, + ViewStyle, +} from 'react-native' +import { TSupportedComponentProps } from './types' + +/** + * @internal + */ +export const scaleStyleObject = ( + style: T, + scaleBy: number, +) => { + style = StyleSheet.flatten(style) as T + + return Object.entries(style ?? {}).reduce( + (acc, [key, value]) => ({ + ...acc, + [key]: Number.isInteger(value) ? value * scaleBy : value, + }), + {}, + ) +} + +/** + * @internal + */ +export const handleContainerLayout = (e: LayoutChangeEvent) => { + const { width, height } = e.nativeEvent.layout + return { + width, + height, + } +} diff --git a/src/components/Cards/commonStyles.ts b/src/components/Cards/commonStyles.ts new file mode 100644 index 000000000..aa307aeb9 --- /dev/null +++ b/src/components/Cards/commonStyles.ts @@ -0,0 +1,23 @@ +import { StyleSheet } from 'react-native' +import { Colors } from '~/utils/colors' +import { Fonts } from '~/utils/fonts' + +export const commonStyles = StyleSheet.create({ + fieldLabelSmall: { + fontSize: 14, + color: Colors.slateGray, + }, + fieldLabel: { + fontSize: 16, + lineHeight: 16, + color: Colors.slateGray, + }, + regularText: { + fontFamily: Fonts.Regular, + color: Colors.black, + }, + mediumText: { + fontFamily: Fonts.Medium, + color: Colors.black, + }, +}) diff --git a/src/components/Cards/getCardDimenstions.ts b/src/components/Cards/getCardDimenstions.ts new file mode 100644 index 000000000..f6c830c0b --- /dev/null +++ b/src/components/Cards/getCardDimenstions.ts @@ -0,0 +1,88 @@ +import { Dimensions } from 'react-native' + +export interface DefiningOptionScreenWidth { + originalScreenWidth: number + containerWidth?: never +} + +export interface DefiningOptionAvailabeWidth { + containerWidth: number + originalScreenWidth?: never +} + +export type DefiningOption = + | DefiningOptionScreenWidth + | DefiningOptionAvailabeWidth + +export type CardDimensions = { + scaledWidth: number + scaledHeight: number + scaleBy: number +} + +function isOriginalScreenWidthDefining( + definingOption: DefiningOption, +): definingOption is DefiningOptionScreenWidth { + return ( + 'originalScreenWidth' in definingOption && + !('containerWidth' in definingOption) + ) +} + +function isContainerWidthDefining( + definingOption: DefiningOption, +): definingOption is DefiningOptionAvailabeWidth { + return ( + 'containerWidth' in definingOption && + !('originalScreenWidth' in definingOption) + ) +} + +/** + * A hook to define how to scale card visual properties. + * It should either define scale by property by: + * - originalScreenWidth + * - widthAvailable + * @param originalCardWidth - a width of card for iPhoneX + * @param definingOption - an option that defines how scaled by return param is being calculated + */ +export const getCardDimensions = ( + originalCardWidth: number, + originalCardHeight: number, + definingOption: DefiningOption, +): CardDimensions => { + // when originalScreenWidth should define scaleBy property + const baseAspectRation = originalCardWidth / originalCardHeight + + let scaleBy: number, cardWidth: number, cardHeight: number + + if (isOriginalScreenWidthDefining(definingOption)) { + scaleBy = + Dimensions.get('screen').width / definingOption.originalScreenWidth + cardWidth = originalCardWidth * scaleBy + } else if (isContainerWidthDefining(definingOption)) { + if (!definingOption.containerWidth) { + scaleBy = 1 + cardWidth = originalCardWidth + } else { + scaleBy = definingOption.containerWidth / originalCardWidth + cardWidth = definingOption.containerWidth + } + } else { + throw new Error( + '"definigOption" param in getCardDimensions fn was used incorrectly', + ) + } + + /** + * in case screen is larger than originalScreenWidth + * limit card width to originalCardWidth value + */ + cardHeight = (1 / baseAspectRation) * cardWidth! + + return { + scaledWidth: cardWidth, + scaledHeight: cardHeight, + scaleBy, + } +} diff --git a/src/components/Cards/hooks.ts b/src/components/Cards/hooks.ts new file mode 100644 index 000000000..01d88657f --- /dev/null +++ b/src/components/Cards/hooks.ts @@ -0,0 +1,208 @@ +import { DisplayVal } from '@jolocom/sdk/js/credentials' +import React, { useMemo, useRef, useState } from 'react' +import { TextLayoutEvent } from '~/types/props' + +/** + * logic to define if credential text should be scaled + */ +export const useCredentialNameScale = () => { + const [isCredentialNameScaled, setIsCredentialNameScaled] = useState(false) + const handleCredentialNameTextLayout = (e: TextLayoutEvent) => { + if (!isCredentialNameScaled) { + setIsCredentialNameScaled(e.nativeEvent.lines.length > 2) + } + } + return { + isCredentialNameScaled, + handleCredentialNameTextLayout, + } +} + +/** + * a hook to decide about nr of displayed fields for document + * section cards + */ +export const useTrimFields = ( + fields: FP[], + photo?: string, + highlight?: string, +) => { + const [displayedFields, setDisplayedFields] = useState(fields.slice(0, 3)) + const lines = useRef(0) + const handleOptionalFieldTextLayout = () => { + let calculatedTimes = 0 + return (e: TextLayoutEvent) => { + calculatedTimes++ + // disable lines manipulation if the number of times this function was invoked + // exceeds length of optional fields twice (because we calculate field name and + // field value ) + if (calculatedTimes < fields.length * 2 + 1) { + const numberOfLines = e.nativeEvent.lines.length + lines.current += numberOfLines + if (calculatedTimes === fields.length * 2) { + /* check wether to show last optional field */ + if (lines.current > 7 && (highlight || photo)) { + setDisplayedFields((prevState) => + prevState.slice(0, Math.floor(lines.current / fields.length)), + ) + } else if (lines.current > 9 && !highlight) { + setDisplayedFields((prevState) => prevState.slice(0, 3)) + } + } + } + } + } + const onTextLayoutChange = handleOptionalFieldTextLayout() + return { displayedFields, onTextLayoutChange } +} + +/** + * a hook to calculate nr of displayed fields for interaction cards + */ +export const useCalculateFieldLines = (maxLinesPerField = 2) => { + const [fieldLines, setFieldLines] = useState>({}) + const handleFieldValueLayout = (e: TextLayoutEvent, idx: number) => { + const lines = e.nativeEvent.lines.length + setFieldLines((prevState) => { + const value = prevState[idx] ?? lines + return { + ...prevState, + [idx]: value > maxLinesPerField ? maxLinesPerField : value, + } + }) + } + return { + fieldLines, + handleFieldValueLayout, + } +} + +/** + * + * @param fields + * @param maxNrFields + * @param maxNrFieldLines + * @returns + */ + +/** + * NOTE: this hook is currently used in + * DocumentSectionDocumentCard and InteractionShareOtherCard components. + * @param fields - claims of a credential + * @param maxNrField - max number of fields a card can display + * @param maxNrFieldLines - max number of lines each field can display + * @returns displayFields - pruned field length based on maxNrField argument + * @returns handleFieldValueLayout - fn executed in onTextLayout event of field value text + * to calculate how many lines each field value has + * @returns handleFieldValuesVisibility - used to decide how many fields + * and how many field lines in field value are displayed. Should it thould beused in FieldCalculator component + */ +export const usePruneFields = ( + fields: Array>, + maxNrFields: number, + maxNrFieldLines: number, +) => { + const displayedFields = useMemo( + () => fields.slice(0, maxNrFields), + [fields.length], + ) + const { fieldLines, handleFieldValueLayout } = useCalculateFieldLines() + + const sumFieldLines = useMemo(() => { + if (Object.keys(fieldLines).length === displayedFields.length) { + return Object.keys(fieldLines).reduce( + (acc, key) => acc + fieldLines[parseInt(key)], + 0, + ) + } + return 0 + }, [JSON.stringify(fieldLines)]) + + /** + * NOTE: We can not display more than maxNrFieldLines lines of all field value lines + */ + /** + * + * @param child a field node (a fragement with 4 children) of the collection of displayfields node + * children: + * 0 idx - top padding of a field + * 1 idx - field label + * 2 idx - padding between field label and field value + * 3 idx - field value + * @param idx an index of a field node in collection of displayedFields + * @returns + * - a field node if sum of displayed filed lines doesnt exceeed maxNrFieldLines + * - null if no space is left because all available field lines were occupied + * - modiedied field node, where field value display number of remaining available fields (nr = 1) + */ + const handleFieldValuesVisibility = (child: React.ReactNode, idx: number) => { + /** + * Once nr of lines of all displayed fields was calculated + */ + if ( + Object.keys(fieldLines).length === displayedFields.length && + sumFieldLines !== 0 + ) { + /** + * if sum of all field lines doesn't exceed max + * amount of lines that can be displayed + */ + if (sumFieldLines <= maxNrFieldLines) { + return child + } else { + /** + * safely display lines of first and second fields, + * because max number of lines for a field value is 2, + * and even if both of these fields (1st, 2nd) + * have max amount of field lines display it will still display 4 lines + */ + if (idx === 0 || idx === 1) { + return child + } else { + const remainingNrLines = + maxNrFieldLines - fieldLines[0] - fieldLines[1] + /** + * If no lines are left to display do not display the whole field + */ + if (remainingNrLines <= 0) { + return null + } else { + /** + * otherwise, + * change numberOfLines display for field value + */ + return React.Children.map(child.props.children, (c, idx) => { + /** + * NOTE: performing operation on the last child as this is a components + * responsible for rendering field.value, + * NOTE: !!!! if texts will get reorganized this will have to be updated + */ + /** + * NOTE: as of now children of field caculator fragment are: + * 0 idx - top padding of a field + * 1 idx - field label + * 2 idx - padding between field label and field value + * 3 idx - field value + */ + if (idx === child.props.children.length - 1) { + return React.cloneElement(c, { + numberOfLines: + maxNrFieldLines - fieldLines[0] - fieldLines[1], + }) + } + // the rest of children + return c + }) + } + } + } + } + return child + } + + return { + displayedFields, + handleFieldValueLayout, + handleFieldValuesVisibility, + } +} diff --git a/src/components/Collapsible/types.ts b/src/components/Collapsible/types.ts index 5158d9482..128a7037c 100644 --- a/src/components/Collapsible/types.ts +++ b/src/components/Collapsible/types.ts @@ -4,9 +4,9 @@ import { FlatListProps, FlatList, } from 'react-native' -import { IWithCustomStyle } from '../Card/types' import { ForwardRefExoticComponent, RefAttributes, ReactElement } from 'react' import { IJoloKeyboardAwareScrollProps } from '../JoloKeyboardAwareScroll/types' +import { IWithCustomStyle } from '~/types/props' interface IHeaderProps extends IWithCustomStyle { height?: number @@ -19,7 +19,7 @@ interface IListProps { type IScrollViewProps = IWithCustomStyle & ScrollViewProps & IListProps export type IFlatListProps = IWithCustomStyle & - ForwardRefExoticComponent> & + ForwardRefExoticComponent> & RefAttributes> & IListProps & { renderHidingText: () => ReactElement @@ -47,5 +47,5 @@ export interface ICollapsibleContext { headerHeight: number hidingTextHeight: number interpolateYValue: (inputRange: number[], outputRange: number[]) => void - handleScroll: (...args: any[]) => void + handleScroll: (...args: unknown[]) => void } diff --git a/src/components/Dots.tsx b/src/components/Dots.tsx index ebdad29c3..856da3e07 100644 --- a/src/components/Dots.tsx +++ b/src/components/Dots.tsx @@ -1,8 +1,8 @@ import React from 'react' import { StyleSheet, View, TouchableOpacity } from 'react-native' import { usePopupMenu } from '~/hooks/popupMenu' +import { IWithCustomStyle } from '~/types/props' import { Colors } from '~/utils/colors' -import { IWithCustomStyle } from './Card/types' import { IPopupOption } from './PopupMenu' interface IDots extends IWithCustomStyle { diff --git a/src/components/FormContainer.tsx b/src/components/FormContainer.tsx index e07b7b15d..a33253547 100644 --- a/src/components/FormContainer.tsx +++ b/src/components/FormContainer.tsx @@ -8,6 +8,7 @@ import { TouchableOpacity, View } from 'react-native' import { Colors } from '~/utils/colors' import { JoloTextSizes, Fonts } from '~/utils/fonts' import { useAdjustResizeInputMode } from '~/hooks/generic' +import useTranslation from '~/hooks/useTranslation' interface Props { title: string @@ -23,6 +24,7 @@ const FormContainer: React.FC = ({ children, isSubmitDisabled = false, }) => { + const { t } = useTranslation() const navigation = useNavigation() useAdjustResizeInputMode() @@ -52,10 +54,10 @@ const FormContainer: React.FC = ({ color={Colors.white90} customStyles={{ fontFamily: Fonts.Medium }} > - Cancel + {t('CredentialForm.closeBtn')} - + {title} = ({ ...(isSubmitDisabled && { opacity: 0.5 }), }} > - Done + {t('CredentialForm.confirmBtn')} diff --git a/src/components/FormHeader.tsx b/src/components/FormHeader.tsx index feaa1bdcc..ffd45a17d 100644 --- a/src/components/FormHeader.tsx +++ b/src/components/FormHeader.tsx @@ -1,7 +1,7 @@ import React from 'react' import { StyleSheet, TouchableOpacity, View } from 'react-native' import JoloText, { JoloTextWeight } from '~/components/JoloText' -import { strings } from '~/translations' +import useTranslation from '~/hooks/useTranslation' import { Colors } from '~/utils/colors' interface IAction { @@ -29,20 +29,22 @@ const ActionBtn: React.FC = ({ color, onPress, children }) => { } const Cancel: IFormHeaderComposition['Cancel'] = ({ onCancel }) => { + const { t } = useTranslation() return ( ) } const Done: IFormHeaderComposition['Done'] = ({ onSubmit }) => { + const { t } = useTranslation() return ( ) } diff --git a/src/components/Input/InputTextArea.tsx b/src/components/Input/InputTextArea.tsx index a710a3fa4..a3fef3e1b 100644 --- a/src/components/Input/InputTextArea.tsx +++ b/src/components/Input/InputTextArea.tsx @@ -9,7 +9,6 @@ import { // @ts-ignore no typescript support as of yet import ReactNativeHapticFeedback from 'react-native-haptic-feedback' -import { strings } from '~/translations' import BP from '~/utils/breakpoints' import { ITextAreaInputProps } from './types' import { CoreInput } from './CoreInput' @@ -17,6 +16,7 @@ import Block from '../Block' import { Colors } from '~/utils/colors' import JoloText from '../JoloText' import { JoloTextSizes } from '~/utils/fonts' +import useTranslation from '~/hooks/useTranslation' const InputTextArea = React.forwardRef( ( @@ -31,6 +31,7 @@ const InputTextArea = React.forwardRef( }, ref, ) => { + const { t } = useTranslation() const [showLimit, setShowLimit] = useState(false) const [limitCount, setLimitCount] = useState(0) const [limitWarning, setLimitWarning] = useState(false) @@ -85,7 +86,7 @@ const InputTextArea = React.forwardRef( + customStyles?: StyleProp | Animated.WithAnimatedValue animated?: boolean testID?: string ignoreScaling?: boolean diff --git a/src/components/NavigationHeader.tsx b/src/components/NavigationHeader.tsx index 45c214cc7..42e5225ef 100644 --- a/src/components/NavigationHeader.tsx +++ b/src/components/NavigationHeader.tsx @@ -5,8 +5,7 @@ import CloseIcon from '~/assets/svg/CloseIcon' import { useGoBack } from '~/hooks/navigation' import IconBtn from './IconBtn' import { BackArrowIcon } from '~/assets/svg' -import { IWithCustomStyle } from './Card/types' -import BP from '~/utils/breakpoints' +import { IWithCustomStyle } from '~/types/props' export enum NavHeaderType { Back = 'back', diff --git a/src/components/Passcode/PasscodeContainer.tsx b/src/components/Passcode/PasscodeContainer.tsx index 1c71e0a31..fbcf25c8c 100644 --- a/src/components/Passcode/PasscodeContainer.tsx +++ b/src/components/Passcode/PasscodeContainer.tsx @@ -1,26 +1,24 @@ import React from 'react' import { View } from 'react-native' -import { IWithCustomStyle } from '../Card/types' +import { IWithCustomStyle } from '~/types/props' const PasscodeContainer: React.FC = ({ children, customStyles = {}, -}) => { - return ( - - {children} - - ) -} +}) => ( + + {children} + +) export default PasscodeContainer diff --git a/src/components/Passcode/PasscodeForgot.tsx b/src/components/Passcode/PasscodeForgot.tsx index e30a2e412..6f5901a28 100644 --- a/src/components/Passcode/PasscodeForgot.tsx +++ b/src/components/Passcode/PasscodeForgot.tsx @@ -1,18 +1,19 @@ import React from 'react' -import { strings } from '~/translations' import Btn, { BtnTypes } from '~/components/Btn' import { useRedirectTo } from '~/hooks/navigation' import { ScreenNames } from '~/types/screens' +import useTranslation from '~/hooks/useTranslation' const PasscodeForgot = () => { + const { t } = useTranslation() const redirectToPinRecoveryInstruction = useRedirectTo( ScreenNames.PinRecoveryInstructions, ) return ( - {strings.FORGOT_YOUR_PASSCODE} + {t('Lock.forgotBtn')} ) } diff --git a/src/components/Passcode/types.ts b/src/components/Passcode/types.ts index 242a737de..67f871859 100644 --- a/src/components/Passcode/types.ts +++ b/src/components/Passcode/types.ts @@ -1,6 +1,6 @@ import React, { SetStateAction } from 'react' import { BiometryType } from 'react-native-biometrics' -import { IWithCustomStyle } from '../Card/types' +import { IWithCustomStyle } from '~/types/props' export interface IPasscodeProps { onSubmit: (passcode: string) => void | Promise diff --git a/src/components/PopupMenu.tsx b/src/components/PopupMenu.tsx index a7b72f330..345c4606f 100644 --- a/src/components/PopupMenu.tsx +++ b/src/components/PopupMenu.tsx @@ -9,15 +9,16 @@ import { RouteProp, useRoute } from '@react-navigation/core' import { ScreenNames } from '~/types/screens' import ScreenContainer from './ScreenContainer' import { useGoBack, useRedirect } from '~/hooks/navigation' -import { IWithCustomStyle } from './Card/types' import ScreenDismissArea from './ScreenDismissArea' import { TransparentModalsParamsList } from '~/screens/LoggedIn/Main' +import { IWithCustomStyle } from '~/types/props' +import useTranslation from '~/hooks/useTranslation' export interface IPopupOption { title: string navigation?: { screen: ScreenNames - params?: Record + params?: Record } onPress?: () => void } @@ -26,9 +27,9 @@ export interface PopupMenuProps { options: IPopupOption[] } -const SolidBlock: React.FC = ({ children, customStyles }) => { - return {children} -} +const SolidBlock: React.FC = ({ children, customStyles }) => ( + {children} +) const PopupButton: React.FC<{ onPress: () => void }> = ({ onPress, @@ -45,6 +46,7 @@ const PopupButton: React.FC<{ onPress: () => void }> = ({ ) const PopupMenu = () => { + const { t } = useTranslation() const goBack = useGoBack() const redirect = useRedirect() const { bottom } = useSafeArea() @@ -52,6 +54,24 @@ const PopupMenu = () => { useRoute>() .params + const onOptionBtnPress = ({ + navigation, + onPress, + }: { + navigation: IPopupOption['navigation'] + onPress: IPopupOption['onPress'] + }) => { + if (onPress) { + onPress() + } + if (navigation) { + redirect(ScreenNames.Main, { + screen: navigation.screen, + params: navigation.params, + }) + } + goBack() + } return ( @@ -60,15 +80,7 @@ const PopupMenu = () => { {options.map(({ title, navigation, onPress }, i) => ( { - onPress && onPress() - navigation && - redirect(ScreenNames.Main, { - screen: navigation.screen, - params: navigation.params, - }) - goBack() - }} + onPress={() => onOptionBtnPress({ navigation, onPress })} > {title} @@ -81,7 +93,9 @@ const PopupMenu = () => { marginTop: 16, }} > - Close + + {t('Documents.cancelCardOption')} + diff --git a/src/components/Tabs/HistoryTabs.tsx b/src/components/Tabs/HistoryTabs.tsx deleted file mode 100644 index 113a254b8..000000000 --- a/src/components/Tabs/HistoryTabs.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import { strings } from '~/translations' -import TabsContainer from './Container' -import { Tabs } from './Tabs' - -const SUBTABS = [ - { id: 'all', value: strings.ALL }, - { id: 'shared', value: strings.SHARED }, - { id: 'received', value: strings.RECEIVED }, -] - -const HistoryTabs: React.FC = ({ children }) => ( - - - {SUBTABS.map((st) => ( - - ))} - - - {children} - -) - -export default HistoryTabs diff --git a/src/components/Tabs/Tabs.tsx b/src/components/Tabs/Tabs.tsx index d69db9b9d..4a1049a4b 100644 --- a/src/components/Tabs/Tabs.tsx +++ b/src/components/Tabs/Tabs.tsx @@ -10,6 +10,8 @@ export const Tabs: React.FC & ITabsComposition = ({ initialActiveTab, initialActiveSubtab, children, + tabs, + subtabs, }) => { const [activeTab, setActiveTab] = useState(initialActiveTab) const [activeSubtab, setActiveSubtab] = useState(initialActiveSubtab) @@ -20,6 +22,8 @@ export const Tabs: React.FC & ITabsComposition = ({ activeSubtab, setActiveTab, setActiveSubtab, + tabs, + subtabs, }), [activeTab, activeSubtab], ) diff --git a/src/components/Tabs/types.ts b/src/components/Tabs/types.ts index cbe5b84b6..263bed171 100644 --- a/src/components/Tabs/types.ts +++ b/src/components/Tabs/types.ts @@ -3,6 +3,8 @@ export interface ITabsContext { activeSubtab: ITabProps | undefined setActiveTab: (value: ITabProps) => void setActiveSubtab: (value: ITabProps) => void + tabs?: ITabProps[] + subtabs?: ITabProps[] } export interface ITabProps { @@ -35,4 +37,6 @@ export interface ITabsComposition { export interface ITabs { initialActiveTab?: ITabProps initialActiveSubtab?: ITabProps + tabs?: ITabProps[] + subtabs?: ITabProps[] } diff --git a/src/components/Widget/Field.tsx b/src/components/Widget/Field.tsx index dff57d83d..7567e0f46 100644 --- a/src/components/Widget/Field.tsx +++ b/src/components/Widget/Field.tsx @@ -2,12 +2,12 @@ import React from 'react' import { StyleSheet, TouchableOpacity, View, TextStyle } from 'react-native' import { PurpleTickSuccess } from '~/assets/svg' -import { strings } from '~/translations' import { Colors } from '~/utils/colors' import { JoloTextSizes } from '~/utils/fonts' import { useWidget } from './context' -import { IWithCustomStyle } from '../Card/types' import JoloText from '../JoloText' +import { IWithCustomStyle } from '~/types/props' +import useTranslation from '~/hooks/useTranslation' export type TField = IFieldComposition & React.FC @@ -53,39 +53,37 @@ const FieldText: React.FC< ) } -const StaticField: React.FC> = ({ value }) => { - return ( - - - - - - ) -} +const StaticField: React.FC> = ({ value }) => ( + + + + + +) const SelectableField: React.FC< Pick -> = ({ value, isSelected, onSelect }) => { - return ( - - - - {isSelected ? ( - - - - ) : ( - - )} - - - ) -} +> = ({ value, isSelected, onSelect }) => ( + + + + {isSelected ? ( + + + + ) : ( + + )} + + +) const EmptyField: React.FC = ({ children }) => { + const { t } = useTranslation() const widgetContext = useWidget() - if (!widgetContext?.onAdd) + if (!widgetContext?.onAdd) { throw new Error('No method provided for creating new attribute') + } return ( @@ -93,7 +91,10 @@ const EmptyField: React.FC = ({ children }) => { {children ? ( children ) : ( - + )} @@ -103,13 +104,11 @@ const EmptyField: React.FC = ({ children }) => { const FieldContainer: React.FC = ({ children, customStyles, -}) => { - return {children} -} +}) => {children} -const Field: React.FC & IFieldComposition = ({ children }) => { - return {children} -} +const Field: React.FC & IFieldComposition = ({ children }) => ( + {children} +) const styles = StyleSheet.create({ container: { diff --git a/src/components/Widget/HeaderAction.tsx b/src/components/Widget/HeaderAction.tsx index e205940de..01d216d08 100644 --- a/src/components/Widget/HeaderAction.tsx +++ b/src/components/Widget/HeaderAction.tsx @@ -1,13 +1,14 @@ import React from 'react' import { StyleSheet, TouchableOpacity, View } from 'react-native' import { PlusIcon } from '~/assets/svg' -import { strings } from '~/translations' import { Colors } from '~/utils/colors' import { JoloTextSizes } from '~/utils/fonts' import { useWidget } from './context' import JoloText, { JoloTextKind } from '../JoloText' +import useTranslation from '~/hooks/useTranslation' const CreateNew: React.FC = () => { + const { t } = useTranslation() const widgetContext = useWidget() if (!widgetContext?.onAdd) { throw new Error('No onCreate prop passed to the widget') @@ -27,7 +28,7 @@ const CreateNew: React.FC = () => { size={JoloTextSizes.middle} color={Colors.white} > - {strings.ADD_ATTRIBUTE} + {t('Identity.addClaimBtn')} ) diff --git a/src/components/Wizard/WizardForm.tsx b/src/components/Wizard/WizardForm.tsx index 38da17bdb..61c125a28 100644 --- a/src/components/Wizard/WizardForm.tsx +++ b/src/components/Wizard/WizardForm.tsx @@ -15,11 +15,13 @@ import JoloText from '../JoloText' import { JoloTextSizes } from '~/utils/fonts' import WizardBody from './WizardBody' import WizardFooter from './WizardFooter' +import useTranslation from '~/hooks/useTranslation' const AutofocusInput = withNextInputAutoFocusInput(Input.Block) const AutofocusContainer = withNextInputAutoFocusForm(View) const WizardForm: React.FC = ({ step, onSubmit }) => { + const { t } = useTranslation() const { config, setActiveStep, isLastStep } = useWizard() const { form: formConfig, validationSchema } = config[step] const initialValues = assembleFormInitialValues(formConfig.fields) @@ -54,7 +56,8 @@ const WizardForm: React.FC = ({ step, onSubmit }) => { // @ts-ignore value={values[field.key]} updateInput={(v) => setFieldValue(field.key, v.trimLeft())} - placeholder={field.label} + // @ts-expect-error terms + placeholder={t(field.label)} autoFocus={idx === 0} withHighlight={ !Boolean(errors[field.key]) && Boolean(values[field.key]) @@ -69,7 +72,8 @@ const WizardForm: React.FC = ({ step, onSubmit }) => { /> {errors[field.key] && ( - {errors[field.key]} + {/* @ts-expect-error terms */} + {t(errors[field.key])} )} diff --git a/src/config/claims.ts b/src/config/claims.ts index 83f326a03..e2ca402ab 100644 --- a/src/config/claims.ts +++ b/src/config/claims.ts @@ -6,7 +6,6 @@ import { AttributeKeys, ClaimKeys, } from '~/types/credentials' -import { strings } from '~/translations' import { emailValidation, nameValidation, @@ -24,12 +23,12 @@ const numberPadKeyboardType: KeyboardTypeOptions = Platform.select({ // TODO: add input validation for each field const emailConfig: IAttributeConfig = { key: AttributeKeys.emailAddress, - label: strings.EMAIL, + label: 'Identity.emailLabel', metadata: claimsMetadata[AttributeKeys.emailAddress], fields: [ { key: ClaimKeys.email, - label: strings.EMAIL, + label: 'Identity.emailLabel', keyboardOptions: { keyboardType: 'email-address', autoCapitalize: 'none', @@ -41,12 +40,12 @@ const emailConfig: IAttributeConfig = { const postalAddressConfig: IAttributeConfig = { key: AttributeKeys.postalAddress, - label: strings.ADDRESS, + label: 'Identity.addressLabel', metadata: claimsMetadata[AttributeKeys.postalAddress], fields: [ { key: ClaimKeys.addressLine, - label: strings.ADDRESS_LINE_FIELD, + label: 'Placeholder.addressLine', keyboardOptions: { keyboardType: 'default', autoCapitalize: 'sentences', @@ -54,7 +53,7 @@ const postalAddressConfig: IAttributeConfig = { }, { key: ClaimKeys.postalCode, - label: strings.POSTAL_CODE_FIELD, + label: 'Placeholder.postalCode', keyboardOptions: { keyboardType: numberPadKeyboardType, autoCapitalize: 'none', @@ -62,7 +61,7 @@ const postalAddressConfig: IAttributeConfig = { }, { key: ClaimKeys.city, - label: strings.CITY_FIELD, + label: 'Placeholder.city', keyboardOptions: { keyboardType: 'default', autoCapitalize: 'sentences', @@ -70,7 +69,7 @@ const postalAddressConfig: IAttributeConfig = { }, { key: ClaimKeys.country, - label: strings.COUNTRY_FIELD, + label: 'Placeholder.country', keyboardOptions: { keyboardType: 'default', autoCapitalize: 'words', @@ -82,12 +81,12 @@ const postalAddressConfig: IAttributeConfig = { const mobileNumberConfig: IAttributeConfig = { key: AttributeKeys.mobilePhoneNumber, - label: strings.NUMBER, + label: 'Identity.phoneNumberLabel', metadata: claimsMetadata[AttributeKeys.mobilePhoneNumber], fields: [ { key: ClaimKeys.telephone, - label: strings.NUMBER, + label: 'Identity.phoneNumberLabel', keyboardOptions: { keyboardType: numberPadKeyboardType, autoCapitalize: 'none', @@ -99,12 +98,12 @@ const mobileNumberConfig: IAttributeConfig = { const nameConfig: IAttributeConfig = { key: AttributeKeys.name, - label: strings.NAME, + label: 'Identity.nameLabel', metadata: claimsMetadata[AttributeKeys.name], fields: [ { key: ClaimKeys.givenName, - label: strings.GIVEN_NAME_FIELD, + label: 'InputPlaceholder.givenName', keyboardOptions: { keyboardType: 'default', autoCapitalize: 'words', @@ -112,7 +111,7 @@ const nameConfig: IAttributeConfig = { }, { key: ClaimKeys.familyName, - label: strings.FAMILY_NAME_FIELD, + label: 'InputPlaceholder.familyName', keyboardOptions: { keyboardType: 'default', autoCapitalize: 'words', diff --git a/src/config/records.ts b/src/config/records.ts deleted file mode 100644 index d5fbb98c9..000000000 --- a/src/config/records.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { FlowType } from '@jolocom/sdk' -import { strings } from '~/translations' - -export interface IRecordConfig { - title: string - steps: { - finished: string[] - unfinished: string[] - } -} - -// NOTE: the first unfinished step will never be used, due to the fact -// that there is always a request i.e. first step. -export const recordConfig: Partial> = { - [FlowType.Authentication]: { - title: strings.AUTHENTICATION, - steps: { - finished: [strings.REQUESTED, strings.CONFIRMED], - unfinished: [strings.NOT_REQUESTED, strings.NOT_CONFIRMED], - }, - }, - [FlowType.Authorization]: { - title: strings.AUTHORIZATION, - steps: { - finished: [strings.AUTHORIZED, strings.CONFIRMED], - unfinished: [strings.NOT_AUTHORIZED, strings.NOT_CONFIRMED], - }, - }, - [FlowType.CredentialOffer]: { - title: strings.INCOMING_OFFER, - steps: { - finished: [strings.OFFERED, strings.SELECTED, strings.ISSUED], - unfinished: [ - strings.NOT_OFFERED, - strings.NOT_SELECTED, - strings.NOT_ISSUED, - ], - }, - }, - [FlowType.CredentialShare]: { - title: strings.INCOMING_REQUEST, - steps: { - finished: [strings.REQUESTED, strings.SHARED], - unfinished: [strings.NOT_REQUESTED, strings.NOT_SHARED], - }, - }, -} diff --git a/src/config/validation.ts b/src/config/validation.ts index e48414758..e7ebe1234 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -1,6 +1,5 @@ import * as yup from 'yup' import { ObjectSchema } from 'yup' -import { strings } from '~/translations' import { ClaimKeys } from '~/types/credentials' import { InputValidation, regexValidations } from '~/utils/stringUtils' @@ -23,14 +22,14 @@ yup.addMethod(yup.string, 'phone', function () { return this.matches( // NOTE: regex for + or +1232312312312 -> to allow only numbers regexValidations[InputValidation.phone], - strings.ONLY_NUMBERS, + 'Validations.onlyNumbers', ) }) -yup.addMethod(yup.string, 'customEmail', function() { +yup.addMethod(yup.string, 'customEmail', function () { return this.matches( regexValidations[InputValidation.email], - strings.EMAIL_FORMAT_ERROR + 'Validations.wrongEmail', ) }) @@ -42,39 +41,46 @@ export const nameValidation = yup }) .atLeastOneOf( [ClaimKeys.givenName, ClaimKeys.familyName], - strings.AT_LEAST_ONE_ERROR, + 'Validations.atLeastOneValue', ) export const emailValidation = yup.object().shape({ [ClaimKeys.email]: yup .string() .customEmail() - .max(100, strings.LARGE) - .required(strings.VALUE_MISSING), + .max(100, 'Validations.tooMany') + .required('Validations.missingValue'), }) export const postalAddressValidation = yup.object().shape({ - [ClaimKeys.addressLine]: yup.string().required(strings.VALUE_MISSING), - [ClaimKeys.postalCode]: yup.string().required(strings.VALUE_MISSING), - [ClaimKeys.city]: yup.string().required(strings.VALUE_MISSING), - [ClaimKeys.country]: yup.string().required(strings.VALUE_MISSING), + [ClaimKeys.addressLine]: yup.string().required('Validations.missingValue'), + [ClaimKeys.postalCode]: yup.string().required('Validations.missingValue'), + [ClaimKeys.city]: yup.string().required('Validations.missingValue'), + [ClaimKeys.country]: yup.string().required('Validations.missingValue'), }) export const mobileNumberValidation = yup.object().shape({ - [ClaimKeys.telephone]: yup.string().phone().required(strings.VALUE_MISSING).min(7, strings.SHORT).max(17, strings.LARGE), + [ClaimKeys.telephone]: yup + .string() + .phone() + .required('Validations.missingValue') + .min(7, 'Validations.tooFew') + .max(17, 'Validations.tooMany'), }) export const contactValidation = yup .object() .shape({ - [ClaimKeys.email]: yup.string().email(strings.EMAIL_FORMAT_ERROR), + [ClaimKeys.email]: yup.string().email('Validations.wrongEmail'), [ClaimKeys.telephone]: yup.string().phone(), }) .atLeastOneOf( [ClaimKeys.email, ClaimKeys.telephone], - strings.AT_LEAST_ONE_ERROR, + 'Validations.atLeastOneValue', ) export const companyValidation = yup.object().shape({ - [ClaimKeys.legalCompanyName]: yup.string().required(strings.VALUE_MISSING), + [ClaimKeys.legalCompanyName]: yup + .string() + .required('Validations.missingValue'), }) diff --git a/src/errors/ErrorBoundary.tsx b/src/errors/ErrorBoundary.tsx index e46872444..3fddf7f74 100644 --- a/src/errors/ErrorBoundary.tsx +++ b/src/errors/ErrorBoundary.tsx @@ -1,12 +1,11 @@ import React from 'react' +import { withTranslation, WithTranslation } from 'react-i18next' import RNRestart from 'react-native-restart' import { ErrorFallback } from '~/components/ErrorFallback' -import Btn, { BtnTypes, BtnSize } from '~/components/Btn' -import { strings } from '~/translations/strings' import { ErrorContext, ErrorScreens } from './errorContext' -export class ErrorBoundary extends React.Component { +class ErrorBoundary extends React.Component { static contextType = ErrorContext public state = { @@ -32,14 +31,15 @@ export class ErrorBoundary extends React.Component { } render() { + const { t } = this.props if (this.state.hasError) { return ( ) @@ -47,3 +47,5 @@ export class ErrorBoundary extends React.Component { return this.props.children } } + +export default withTranslation()(ErrorBoundary) diff --git a/src/errors/codes.ts b/src/errors/codes.ts index 352565636..7fecccb58 100644 --- a/src/errors/codes.ts +++ b/src/errors/codes.ts @@ -1,5 +1,3 @@ -import { strings } from '~/translations' - export enum SWErrorCodes { SWUnknown = 'SWUnknown', SWAgentNotFound = 'SWAgentNotFound', @@ -10,33 +8,3 @@ export enum SWErrorCodes { SWInteractionUnknownError = 'SWInteractionUnknownError', SWInteractionOfferAllInvalid = 'SWInteractionOfferAllInvalid', } - -export const UIErrors: Partial< - Record -> = { - [SWErrorCodes.SWUnknown]: { - title: strings.UNKNOWN_ERROR, - message: - strings.AND_IF_THIS_IS_NOT_THE_FIRST_TIME_WE_STRONGLY_RECOMMEND_LETTING_US_KNOW, - }, - [SWErrorCodes.SWInteractionUnknownError]: { - title: strings.INTERACTION_ERROR_TITLE, - message: strings.INTERACTION_ERROR_MESSAGE, - }, - [SWErrorCodes.SWInteractionOfferAllInvalid]: { - title: strings.OFFER_ALL_INVALID_TOAST_TITLE, - message: strings.OFFER_ALL_INVALID_TOAST_MSG, - }, -} - -interface IUIError extends Error { - message: SWErrorCodes -} - -export function isUIError(error: Error): error is IUIError { - return Object.values(SWErrorCodes).includes(error.message as SWErrorCodes) -} - -export function isError(error: any): error is Error { - return error instanceof Error -} diff --git a/src/errors/errorContext.tsx b/src/errors/errorContext.tsx index cc6e6e1c7..1f0e1d132 100644 --- a/src/errors/errorContext.tsx +++ b/src/errors/errorContext.tsx @@ -8,15 +8,26 @@ export enum ErrorScreens { errorDisplay = 'errorDisplay', } +export interface ErrorDetails { + title: string + message: string +} + interface IErrorContext { - error: Error | null - errorScreen: ErrorScreens | null - setError: (screen: ErrorScreens | null, err: Error | null) => void + error?: Error + errorScreen?: ErrorScreens + errorDetails?: ErrorDetails + setError: ( + screen?: ErrorScreens, + err?: Error, + errorDetails?: ErrorDetails, + ) => void } const initialState = { - error: null, - errorScreen: null, + error: undefined, + errorScreen: undefined, + errorDetails: undefined, setError: () => {}, } @@ -24,19 +35,21 @@ export const ErrorContext = createContext(initialState) export const ErrorContextProvider: React.FC = ({ children }) => { const [state, setState] = useState< - Pick + Pick >({ - error: null, - errorScreen: null, + error: undefined, + errorScreen: undefined, + errorDetails: undefined, }) const contextValue = useMemo( () => ({ ...state, setError: ( - errorScreen: ErrorScreens | null, - error: Error | null = null, - ) => setState({ error, errorScreen }), + errorScreen?: ErrorScreens, + error?: Error, + errorDetails?: ErrorDetails, + ) => setState({ error, errorScreen, errorDetails }), }), [JSON.stringify({ state, setState })], ) diff --git a/src/errors/modals/ErrorDisplay.tsx b/src/errors/modals/ErrorDisplay.tsx index e7a6b3aea..35e881165 100644 --- a/src/errors/modals/ErrorDisplay.tsx +++ b/src/errors/modals/ErrorDisplay.tsx @@ -3,17 +3,15 @@ import ModalScreen from '~/modals/Modal' import useErrors from '~/hooks/useErrors' import { ErrorScreens } from '../errorContext' import { ErrorFallback } from '~/components/ErrorFallback' -import { SWErrorCodes, UIErrors } from '../codes' -import { strings } from '~/translations' import { useSelector } from 'react-redux' import { getIsAppLocked } from '~/modules/account/selectors' +import useTranslation from '~/hooks/useTranslation' const ErrorDisplay = () => { + const { t } = useTranslation() const isAppLocked = useSelector(getIsAppLocked) - const { errorScreen, resetError, error, showErrorReporting } = useErrors() - const { title, message } = - UIErrors[error?.message as SWErrorCodes] ?? - UIErrors[SWErrorCodes.SWUnknown]! + const { errorScreen, resetError, showErrorReporting, errorDetails } = + useErrors() return ( { animationType={'slide'} > ) diff --git a/src/errors/modals/ErrorReporting.tsx b/src/errors/modals/ErrorReporting.tsx index 94424aa90..18254c038 100644 --- a/src/errors/modals/ErrorReporting.tsx +++ b/src/errors/modals/ErrorReporting.tsx @@ -17,33 +17,19 @@ import useConnection from '~/hooks/connection' import { useSuccess } from '~/hooks/loader' import useSentry from '~/hooks/sentry' import useErrors from '~/hooks/useErrors' +import useTranslation from '~/hooks/useTranslation' import ModalScreen from '~/modals/Modal' import { getIsAppLocked } from '~/modules/account/selectors' import Dropdown from '~/screens/LoggedIn/Settings/components/Dropdown' import Section from '~/screens/LoggedIn/Settings/components/Section' -import { strings } from '~/translations' import { Colors } from '~/utils/colors' import { JoloTextSizes } from '~/utils/fonts' import { SCREEN_HEADER_HEIGHT } from '~/utils/screenSettings' import { InputValidation, regexValidations } from '~/utils/stringUtils' import { ErrorScreens } from '../errorContext' -const INQUIRIES_LIST = [ - strings.NO_INTERNET_CONNECTION, - strings.THE_APP_KEEPS_CRASHING, - strings.CANT_LOGIN, - strings.BACKUP_IS_EMPTY, - strings.PROBLEMS_WITH_THE_INTERFACE, - strings.SOMETHING_DOESNT_SEEM_RIGHT, - strings.OTHER, -] - -const DROPDOWN_OPTIONS = INQUIRIES_LIST.map((el) => ({ - id: el.split(' ').join(''), - value: el, -})) - const ErrorReporting = () => { + const { t } = useTranslation() const isAppLocked = useSelector(getIsAppLocked) const { errorScreen, resetError } = useErrors() const { sendErrorReport } = useSentry() @@ -67,6 +53,21 @@ const ErrorReporting = () => { const [contactValue, setContactValue] = useState('') const [contactValid, setContactValid] = useState(true) + const INQUIRIES_LIST = [ + t('ErrorReporting.issueOption_1'), + t('ErrorReporting.issueOption_2'), + t('ErrorReporting.issueOption_3'), + t('ErrorReporting.issueOption_4'), + t('ErrorReporting.issueOption_5'), + t('ErrorReporting.issueOption_6'), + t('ErrorReporting.issueOption_7'), + ] + + const DROPDOWN_OPTIONS = INQUIRIES_LIST.map((el) => ({ + id: el.split(' ').join(''), + value: el, + })) + const assembledData = { issue: selectedIssue, details: detailsInput, @@ -143,11 +144,9 @@ const ErrorReporting = () => { keyboardShouldPersistTaps="handled" >
- - {/* FIXME: string */} - {strings.WHAT_WE_ARE_GOING_TO_TALK_ABOUT} - + {t('ErrorReporting.issueHeader')} @@ -155,15 +154,14 @@ const ErrorReporting = () => {
- {strings.ANYTHING_SPECIFIC_TO_MENTION} + {t('ErrorReporting.detailsHeader')} - {/* FIXME: strings */} - {strings.DARE_TO_SUGGEST_SMTH} + {t('ErrorReporting.detailsSubheader')} {({ focusInput }) => ( @@ -194,13 +192,13 @@ const ErrorReporting = () => { size={JoloTextSizes.mini} customStyles={{ textAlign: 'left', marginBottom: 4 }} > - {strings.INCLUDE_LOGS} + {t('ErrorReporting.logsHeader')} - {strings.ERROR_REPORTING_LOGS_WARNING} + {t('ErrorReporting.logsSubheader')} @@ -208,7 +206,7 @@ const ErrorReporting = () => {
- {strings.WANT_TO_GET_IN_TOUCH} + {t('ErrorReporting.contactHeader')} {({ focusInput }) => ( @@ -216,7 +214,7 @@ const ErrorReporting = () => { validation={regexValidations[InputValidation.email]} value={contactValue} updateInput={setContactValue} - placeholder={strings.CONTACT_US_GET_IN_TOUCH} + placeholder={t('ErrorReporting.contactPlaceholder')} onValidation={handleContactValidation} onFocus={focusInput} /> @@ -229,13 +227,13 @@ const ErrorReporting = () => { customStyles={{ textAlign: 'left', marginTop: 12 }} > {contactValid - ? strings.WE_DO_NOT_STORE_DATA - : strings.PLEASE_ENTER_A_VALID_EMAIL} + ? t('ErrorReporting.contactInputInfo') + : t('ErrorReporting.contactInputError')}
- {strings.ERROR_REPORTING_RATE} + {t('ErrorReporting.rateHeader')}
@@ -245,7 +243,7 @@ const ErrorReporting = () => { onPress={handleSubmit} disabled={!isSubmitEnabled()} > - {strings.SEND} + {t('ErrorReporting.submitBtn')} diff --git a/src/hooks/biometry.ts b/src/hooks/biometry.ts index 17540493b..77a2de8f8 100644 --- a/src/hooks/biometry.ts +++ b/src/hooks/biometry.ts @@ -1,10 +1,25 @@ import Biometry, { BiometryType } from 'react-native-biometrics' -import { getBiometryDescription } from '~/screens/Modals/DeviceAuthentication/utils/getText' +import { BiometryTypes } from '~/screens/Modals/DeviceAuthentication/module/deviceAuthTypes' import { StorageKeys, useAgent } from './sdk' +import useTranslation from './useTranslation' export const useBiometry = () => { + const { t } = useTranslation() const agent = useAgent() + const getBiometryDescription = (biometryType: BiometryType | undefined) => { + switch (biometryType) { + case BiometryTypes.TouchID: + return t('Biometry.promptFingerprint') + case BiometryTypes.FaceID: + return t('Biometry.promptFaceId') + case 'Biometrics': + return t('Biometry.promptBiometrics') + default: + throw new Error('We do not support this type of biometry') + } + } + const authenticate = async (biometryType: BiometryType | undefined) => { return await Biometry.simplePrompt({ promptMessage: getBiometryDescription(biometryType), diff --git a/src/hooks/connection.ts b/src/hooks/connection.ts index 83934d909..890ea2aba 100644 --- a/src/hooks/connection.ts +++ b/src/hooks/connection.ts @@ -1,33 +1,26 @@ import { useNetInfo } from '@react-native-community/netinfo' import { useEffect } from 'react' -import { strings } from '~/translations' import { usePrevious } from './generic' import { useToasts } from './toasts' - -const DISCONNECTED_TOAST = { - title: strings.NOT_CONNECTED, - message: strings.WE_CANT_REACH_YOU, -} - -const CONNECTED_TOAST = { - title: strings.YOU_ARE_BACK_ONLINE, - message: strings.ALL_WALLET_FUNCTIONALITIES, -} +import useTranslation from './useTranslation' const useConnection = () => { + const { t } = useTranslation() const { scheduleWarning, scheduleInfo } = useToasts() const netInfo = useNetInfo() const showDisconnectedToast = () => { scheduleWarning({ - ...DISCONNECTED_TOAST, + title: t('Toasts.noInternetTitle'), + message: t('Toasts.noInternetMsg'), }) } const showConnectedToast = () => { scheduleInfo({ - ...CONNECTED_TOAST, + title: t('Toasts.reconnectedInternetTitle'), + message: t('Toasts.reconnectedInternetMsg'), }) } diff --git a/src/hooks/credentials.ts b/src/hooks/credentials.ts index 81f975780..dbaa125b5 100644 --- a/src/hooks/credentials.ts +++ b/src/hooks/credentials.ts @@ -1,6 +1,9 @@ import { useAgent } from './sdk' import { useDispatch } from 'react-redux' import { deleteCredential } from '~/modules/credentials/actions' +import { ClaimKeys, DisplayCredential } from '~/types/credentials' +import { useTranslation } from 'react-i18next' +import moment from 'moment' export const useDeleteCredential = () => { const agent = useAgent() @@ -11,3 +14,48 @@ export const useDeleteCredential = () => { dispatch(deleteCredential(id)) } } + +export const useCredentialOptionalFields = () => { + const { t } = useTranslation() + + const nonOptionalFields = [ + ClaimKeys.familyName, + ClaimKeys.givenName, + ClaimKeys.id, + ClaimKeys.photo, + ] + + const getOptionalFields = (credential: T) => { + const additionalFields = [ + { + key: 'issued', + label: t('Documents.issuedFieldLabel'), + value: moment(credential.issued).format('DD.MM.YYYY'), + }, + { + key: 'issuer', + label: t('Documents.issuerFieldLabel'), + value: + credential.issuer?.publicProfile?.name ?? credential.issuer?.did!, + }, + { + key: 'expire', + label: t('Documents.expiresFieldLabel'), + value: moment(credential.expires).format('DD.MM.YYYY'), + }, + ] + + if (!credential.properties.length) return additionalFields + + return credential.properties + .filter((p) => !nonOptionalFields.includes(p.key as ClaimKeys)) + .map(({ label, value, key }) => ({ + key, + label: label || t('Documents.unspecifiedField'), + value: value || t('Documents.unspecifiedField'), + })) + .concat(additionalFields) + } + + return { nonOptionalFields, getOptionalFields } +} diff --git a/src/hooks/history/index.ts b/src/hooks/history/index.ts index 9e961c34f..ec0185717 100644 --- a/src/hooks/history/index.ts +++ b/src/hooks/history/index.ts @@ -2,11 +2,12 @@ import { FlowType, Interaction } from '@jolocom/sdk' import { useAgent } from '~/hooks/sdk' import { IRecordDetails, IPreLoadedInteraction } from '~/types/records' -import { getDateSection } from './utils' +import { getDateSection, translateRecordConfig } from './utils' import { RecordAssembler } from '~/middleware/records/recordAssembler' -import { recordConfig } from '~/config/records' +import useTranslation from '../useTranslation' export const useHistory = () => { + const { t } = useTranslation() const agent = useAgent() const getSectionDetails = (interaction: Interaction) => { @@ -14,7 +15,12 @@ export const useHistory = () => { const { issued } = interaction.lastMessage const section = getDateSection(new Date(issued)) - return { type, section, lastUpdate: issued.toString(), id: interaction.id } + return { + type, + section, + lastUpdate: issued.toString(), + id: interaction.id, + } } const getInteractions = async ( @@ -30,9 +36,7 @@ export const useHistory = () => { }) const groupedInteractions = allInteractions.reduce( - (acc, intx) => { - return [...acc, getSectionDetails(intx)] - }, + (acc, intx) => [...acc, getSectionDetails(intx)], [], ) @@ -64,7 +68,7 @@ export const useHistory = () => { summary: interaction.getSummary(), lastMessageDate: issued, expirationDate: expires, - config: recordConfig, + config: translateRecordConfig(t), }) return recordAssembler.getRecordDetails() diff --git a/src/hooks/history/utils.ts b/src/hooks/history/utils.ts index e80e3df1f..4657e9855 100644 --- a/src/hooks/history/utils.ts +++ b/src/hooks/history/utils.ts @@ -1,15 +1,18 @@ +import { TFunction } from 'i18next' import moment from 'moment' +import { FlowType } from 'react-native-jolocom' import { IPreLoadedInteraction, IHistorySection, IHistorySectionData, + IRecordConfig, } from '~/types/records' export const getDateSection = (date: Date) => moment(date).calendar(null, { - sameDay: '[Today]', - lastDay: '[Yesterday]', - lastWeek: '[Last] dddd', + sameDay: '[Dates.today]', + lastDay: '[Dates.yesterday]', + lastWeek: '[Dates.last]dddd', sameElse: 'DD/MM/YYYY', }) @@ -31,3 +34,100 @@ export const groupBySection = ( data: groupedObj[title], })) } + +/** + * NOTE: finished and unfinished states are the same here, + * because 'not' particle in unfinished step is added during + * translation of record config (fn translateRecordConfig) + */ +export const recordConfig = { + status: { + unknown: 'General.unknown', + expired: 'History.expiredState', + pending: 'History.pendingState', + }, + flows: { + [FlowType.Authentication]: { + title: 'History.authenticationHeader', + steps: { + finished: [ + 'History.authenticationRequestStepHeader', + 'History.authResponseStepHeader', + ], + unfinished: [ + 'History.authenticationRequestStepHeader', + 'History.authResponseStepHeader', + ], + }, + }, + [FlowType.Authorization]: { + title: 'History.authzHeader', + steps: { + finished: [ + 'History.authzRequestStepHeader', + 'History.authzResponseStepHeader', + ], + unfinished: [ + 'History.authzRequestStepHeader', + 'History.authzResponseStepHeader', + ], + }, + }, + [FlowType.CredentialOffer]: { + title: 'History.credentialOfferHeader', + steps: { + finished: [ + 'History.offerRequestStepHeader', + 'History.offerResponseStepHeader', + 'History.offerReceiveStepHeader', + ], + unfinished: [ + 'History.offerRequestStepHeader', + 'History.offerResponseStepHeader', + 'History.offerReceiveStepHeader', + ], + }, + }, + [FlowType.CredentialShare]: { + title: 'History.credShareHeader', + steps: { + finished: [ + 'History.credShareRequestStepHeader', + 'History.credShareResponseStepHeader', + ], + unfinished: [ + 'History.credShareRequestStepHeader', + 'History.credShareResponseStepHeader', + ], + }, + }, + }, +} + +type TRecordConfig = Record + +export const translateRecordConfig = (t: TFunction): IRecordConfig => { + function traverseConfig(config: TRecordConfig) { + return Object.keys(config).reduce( + (translatedConfig, key) => { + if (typeof config[key] === 'string') { + translatedConfig[key] = t(config[key]) + } else if (Array.isArray(config[key])) { + translatedConfig[key] = config[key].map((s: string) => { + if (key === 'unfinished') { + return t('History.notFinishedStepHeader', { + text: t(s).toLowerCase(), + }) + } + return t(s) + }) + } else { + translatedConfig[key] = traverseConfig(config[key]) + } + return translatedConfig + }, + {}, + ) + } + return traverseConfig(recordConfig) as IRecordConfig +} diff --git a/src/hooks/interactions/handlers.ts b/src/hooks/interactions/handlers.ts index 437857f5f..0dc0d80f5 100644 --- a/src/hooks/interactions/handlers.ts +++ b/src/hooks/interactions/handlers.ts @@ -18,11 +18,8 @@ import { useNavigation } from '@react-navigation/native' import { ScreenNames } from '~/types/screens' import { useInteractionHandler } from './interactionHandlers' import { useToasts } from '../toasts' -import { isError, isUIError, SWErrorCodes, UIErrors } from '~/errors/codes' import { parseJWT } from '~/utils/parseJWT' import useConnection from '../connection' -import { FlowType, Interaction } from 'react-native-jolocom' -import { InteractionDetails } from '~/modules/interaction/types' export const useInteraction = () => { const agent = useAgent() @@ -38,7 +35,7 @@ export const useInteractionStart = () => { const loader = useLoader() const interactionHandler = useInteractionHandler() const { connected, showDisconnectedToast } = useConnection() - const { scheduleWarning, scheduleErrorWarning } = useToasts() + const { scheduleErrorWarning } = useToasts() return async (jwt: string) => { // NOTE: not continuing the interaction if there is no network connection @@ -68,16 +65,7 @@ export const useInteractionStart = () => { }, { showSuccess: false, showFailed: false }, (error) => { - if (isError(error)) { - // @ts-ignore - if (isUIError(error)) scheduleWarning(UIErrors[error.message]) - else - scheduleErrorWarning(error, { - title: UIErrors[SWErrorCodes.SWInteractionUnknownError]?.title, - message: - UIErrors[SWErrorCodes.SWInteractionUnknownError]?.message, - }) - } + if (error) scheduleErrorWarning(error) }, ) } diff --git a/src/hooks/interactions/interactionHandlers.ts b/src/hooks/interactions/interactionHandlers.ts index 681d662db..9429df440 100644 --- a/src/hooks/interactions/interactionHandlers.ts +++ b/src/hooks/interactions/interactionHandlers.ts @@ -5,15 +5,12 @@ import { } from '@jolocom/sdk/js/interactionManager/types' import { FlowType, Interaction } from 'react-native-jolocom' -import { - getCredentialCategory, -} from '../signedCredentials/utils' +import { getCredentialCategory } from '../signedCredentials/utils' import { useAgent } from '../sdk' import useTranslation from '~/hooks/useTranslation' import { useToasts } from '../toasts' -import { strings } from '~/translations' -import truncateDid from '~/utils/truncateDid' import { CredentialRequestHandler } from '~/middleware/interaction/credentialRequestConstrains' +import { getCounterpartyName } from '~/utils/dataMapping' const authenticationHandler = (state: AuthenticationFlowState) => ({ description: state.description, @@ -22,24 +19,23 @@ const authenticationHandler = (state: AuthenticationFlowState) => ({ const authorizationHandler = (state: AuthorizationFlowState) => ({ action: state.action, description: state.description, - imageURL: state.imageURL + imageURL: state.imageURL, }) -const credentialOfferHandler = (state: CredentialOfferFlowState) => { - return { - credentials: { - service_issued: state.offerSummary.map( - ({ renderInfo, type, credential }) => ({ - type: type, - category: getCredentialCategory(renderInfo), - invalid: false, - name: credential?.name ?? '', - properties: credential?.display?.properties || [], - }), - ), - }, - } -} +const credentialOfferHandler = (state: CredentialOfferFlowState) => ({ + credentials: { + // eslint-disable-next-line + service_issued: state.offerSummary.map( + ({ renderInfo, type, credential }) => ({ + type, + category: getCredentialCategory(renderInfo), + invalid: false, + name: credential?.name ?? '', + properties: credential?.display?.properties || [], + }), + ), + }, +}) /** * 1. Use it to check whatever logic should happen before @@ -56,8 +52,7 @@ export const useInteractionHandler = () => { return async (interaction: Interaction) => { const { state, initiator } = interaction.getSummary() - const serviceName = - initiator.publicProfile?.name ?? truncateDid(initiator.did) + const serviceName = getCounterpartyName(initiator) let flowSpecificData @@ -96,8 +91,8 @@ export const useInteractionHandler = () => { // FIXME: there is an issue with the strings here, will be fixed when the // i18n and PoEditor are properly set up. scheduleWarning({ - title: t(strings.SHARE_MISSING_DOCS_TITLE), - message: t(strings.SHARE_MISSING_DOCS_MSG, { + title: t('Toasts.shareMissingDocsTitle'), + message: t('Toasts.shareMissingDocsMsg', { serviceName, documentType: handler.missingCredentialTypes.join(', '), }), diff --git a/src/hooks/interactions/useCredentialOfferSubmit.ts b/src/hooks/interactions/useCredentialOfferSubmit.ts index 2814e22ed..fc02d5ea5 100644 --- a/src/hooks/interactions/useCredentialOfferSubmit.ts +++ b/src/hooks/interactions/useCredentialOfferSubmit.ts @@ -3,7 +3,6 @@ import { useDispatch, useSelector } from 'react-redux' import { updateOfferValidation } from '~/modules/interaction/actions' import useCredentialOfferFlow from '~/hooks/interactions/useCredentialOfferFlow' import { useToasts } from '../toasts' -import { strings } from '~/translations/strings' import { ScreenNames } from '~/types/screens' import useInteractionToasts from './useInteractionToasts' import { useRedirect } from '../navigation' @@ -12,7 +11,7 @@ import { useCredentials } from '../signedCredentials' import { useFinishInteraction } from './handlers' import { CredentialCategories } from '~/types/credentials' import { SWErrorCodes } from '~/errors/codes' -import { useTranslation } from 'react-i18next' +import useTranslation from '~/hooks/useTranslation' import { getInteractionCounterpartyName } from '~/modules/interaction/selectors' const useCredentialOfferSubmit = () => { @@ -36,7 +35,7 @@ const useCredentialOfferSubmit = () => { const scheduleSuccess = (initialTab: CredentialCategories) => scheduleSuccessInteraction({ interact: { - label: strings.REVIEW, + label: t('Toasts.successfulOfferInteractionBtn'), onInteract: () => redirect(ScreenNames.Documents, { initialTab }), }, }) @@ -70,10 +69,12 @@ const useCredentialOfferSubmit = () => { // NOTE: Uncomment the line below to test the duplicates edge-case // await storeSelectedCredentials() const anyDuplicates = await checkDuplicates() - if (anyDuplicates) - throw new Error( - "Duplicates were found. Can't proceed with the interaction", - ) + if (anyDuplicates) { + return scheduleInfo({ + title: t('Toasts.offerDuplicateTitle'), + message: t('Toasts.offerDuplicateSingleMsg'), + }) + } const validatedCredentials = await getValidatedCredentials() const allValid = validatedCredentials.every((cred) => !cred.invalid) @@ -89,8 +90,8 @@ const useCredentialOfferSubmit = () => { scheduleErrorWarning( new Error(SWErrorCodes.SWInteractionOfferAllInvalid), { - title: strings.OFFER_ALL_INVALID_TOAST_TITLE, - message: t(strings.OFFER_ALL_INVALID_TOAST_MSG, { + title: t('Toasts.offerInvalidDocsTitle'), + message: t('Toasts.offerInvalidDocsMsg', { serviceName: counterpartyName, }), }, @@ -99,8 +100,8 @@ const useCredentialOfferSubmit = () => { } else { dispatch(updateOfferValidation(validatedCredentials)) scheduleInfo({ - title: strings.OFFER_RENEGOTIATION_TITLE, - message: strings.OFFER_RENEGOTIATION_MSG, + title: t('Toasts.offerRenegotiationTitle'), + message: t('Toasts.offerRenegotiationMsg'), }) } } catch (err) { diff --git a/src/hooks/interactions/useInteractionToasts.ts b/src/hooks/interactions/useInteractionToasts.ts index ef20f95df..708961c11 100644 --- a/src/hooks/interactions/useInteractionToasts.ts +++ b/src/hooks/interactions/useInteractionToasts.ts @@ -1,14 +1,15 @@ import { useToasts } from '~/hooks/toasts' import { ToastBody } from '~/types/toasts' -import { strings } from '~/translations/strings' +import useTranslation from '../useTranslation' const useInteractionToasts = () => { + const { t } = useTranslation() const { scheduleInfo } = useToasts() const scheduleSuccessInteraction = (config?: Partial) => scheduleInfo({ - title: strings.INTERACTION_SUCCESS_TOAST_TITLE, - message: strings.INTERACTION_SUCCESS_TOAST_MSG, + title: t('Toasts.successfulInteractionTitle'), + message: t('Toasts.successfulInteractionMsg'), ...config, }) diff --git a/src/hooks/loader.ts b/src/hooks/loader.ts index f61d7aa42..babc22949 100644 --- a/src/hooks/loader.ts +++ b/src/hooks/loader.ts @@ -2,8 +2,8 @@ import { useDispatch } from 'react-redux' import { setLoader, dismissLoader } from '~/modules/loader/actions' import { LoaderTypes } from '~/modules/loader/types' -import { strings } from '~/translations/strings' import { sleep } from '~/utils/generic' +import useTranslation from './useTranslation' export interface LoaderConfig { showFailed?: boolean @@ -13,17 +13,18 @@ export interface LoaderConfig { failed?: string } -const defaultConfig = { - loading: strings.LOADING, - showFailed: true, - showSuccess: true, - success: strings.SUCCESS, - failed: strings.FAILED, -} - export const useLoader = () => { + const { t } = useTranslation() const dispatch = useDispatch() + const defaultConfig = { + loading: t('Loader.loading'), + showFailed: true, + showSuccess: true, + success: t('Loader.successDefault'), + failed: t('Loader.failedDefault'), + } + return async ( callback: () => Promise, config: LoaderConfig = defaultConfig, @@ -84,24 +85,31 @@ export const useLoader = () => { } } -const openLoader = - (type: LoaderTypes, msg: string) => - (delay: number = 2500) => { - const dispatch = useDispatch() +const openLoader = (type: LoaderTypes, msg: string) => (delay: number) => { + const dispatch = useDispatch() - return (onComplete?: () => void) => { - dispatch( - setLoader({ - type, - msg, - }), - ) - setTimeout(() => { - onComplete && onComplete() - dispatch(dismissLoader()) - }, delay) - } + return (onComplete?: () => void) => { + dispatch( + setLoader({ + type, + msg, + }), + ) + setTimeout(() => { + onComplete && onComplete() + dispatch(dismissLoader()) + }, delay) } +} + +export const useSuccess = (delay: number = 2500) => { + const { t } = useTranslation() -export const useSuccess = openLoader(LoaderTypes.success, strings.SUCCESS) -export const useFailed = openLoader(LoaderTypes.error, strings.FAILED) + return openLoader(LoaderTypes.success, t('Loader.successDefault'))(delay) +} + +export const useFailed = (delay: number = 2500) => { + const { t } = useTranslation() + + return openLoader(LoaderTypes.error, t('Loader.failedDefault'))(delay) +} diff --git a/src/hooks/sdk.ts b/src/hooks/sdk.ts index 91ad8db44..27f2115b8 100644 --- a/src/hooks/sdk.ts +++ b/src/hooks/sdk.ts @@ -8,11 +8,11 @@ import { SDKError, Agent } from 'react-native-jolocom' import { AgentContext } from '~/utils/sdk/context' import { useLoader } from './loader' import { setDid, setLogged, setLocalAuth } from '~/modules/account/actions' -import { strings } from '~/translations/strings' import { generateSecureRandomBytes } from '~/utils/generateBytes' import { PIN_SERVICE } from '~/utils/keychainConsts' import useTermsConsent from './consent' import { makeInitializeCredentials, useCredentials } from './signedCredentials' +import useTranslation from './useTranslation' // TODO: add a hook which manages setting/getting properties from storage // and handles their types @@ -158,6 +158,7 @@ export const useSubmitIdentity = () => { const dispatch = useDispatch() const createIdentity = useIdentityCreate() const loader = useLoader() + const { t } = useTranslation() return async () => { await loader( @@ -166,7 +167,7 @@ export const useSubmitIdentity = () => { await agent.storage.store.setting(StorageKeys.encryptedSeed, {}) }, { - loading: strings.CREATING, + loading: t('SeedphraseRepeat.confirmLoader'), }, (error) => dispatch(setLogged(!Boolean(error))), ) diff --git a/src/hooks/signedCredentials/utils.ts b/src/hooks/signedCredentials/utils.ts index ef9fba213..bb5bf71e5 100644 --- a/src/hooks/signedCredentials/utils.ts +++ b/src/hooks/signedCredentials/utils.ts @@ -2,7 +2,7 @@ import { IdentitySummary } from '@jolocom/sdk' import { CredentialIssuer } from '@jolocom/sdk/js/credentials' import { SignedCredential } from 'jolocom-lib/js/credentials/signedCredential/signedCredential' import { AttributeI, AttrsState } from '~/modules/attributes/types' -import { strings } from '~/translations' +import { CredentialUITypes } from '~/types/credentials' import { AttributeTypes, BaseUICredential, @@ -33,14 +33,20 @@ export const getCredentialUIType = (type: string) => { switch (type) { case IdentificationTypes.ProofOfIdCredentialDemo: case IdentificationTypes.ProofOfDriverLicenceDemo: - return strings.IDENTIFICATION + return CredentialUITypes.identification case TicketTypes.ProofOfTicketDemo: - return strings.TICKET + return CredentialUITypes.tickets default: - return strings.UNKNOWN + return CredentialUITypes.unknown } } +export const uiTypesTerms = { + [CredentialUITypes.identification]: 'Documents.identificationCategory', + [CredentialUITypes.tickets]: 'Documents.ticketsCategory', + [CredentialUITypes.unknown]: 'General.unknown', +} + export const separateCredentialsAndAttributes = ( allCredentials: SignedCredential[], did: string, @@ -89,8 +95,8 @@ export async function mapCredentialsToDisplay( properties: properties ? properties.map((p, idx) => ({ key: p.key ? p.key.split('.')[1] : `${Date.now()}${idx}}`, - label: p.label ?? strings.NOT_SPECIFIED, - value: p.value || strings.NOT_SPECIFIED, + label: p.label ?? '', + value: p.value || '', })) : [], } @@ -121,7 +127,7 @@ export function mapDisplayToCustomDisplay( .split(' ') .filter((e) => Boolean(e)) .join(' ') - : strings.ANONYMOUS + : '' updatedProperties = updatedProperties.filter( (p) => @@ -259,14 +265,14 @@ export const reduceCustomDisplayCredentialsByType = < * Used in: * * documents */ -export const reduceCustomDisplayCredentialsByIssuer = < +const reduceCustomDisplayCredentialsByIssuer = < T extends { issuer: IdentitySummary }, >( credentials: T[], ): Array> => { return credentials.reduce( (groupedCredentials: Array>, cred: T) => { - const issuer = cred.issuer.publicProfile?.name ?? strings.UNKNOWN + const issuer = cred.issuer.publicProfile?.name ?? '' const group = groupedCredentials.filter((c) => c.value === issuer) if (group.length) { groupedCredentials = groupedCredentials.map((g) => { diff --git a/src/hooks/toasts.ts b/src/hooks/toasts.ts index ff54a18d6..c36aaa568 100644 --- a/src/hooks/toasts.ts +++ b/src/hooks/toasts.ts @@ -9,10 +9,11 @@ import { ToastBody, } from '~/types/toasts' import { scheduleToast, removeToastAndUpdate } from '~/modules/toasts/actions' -import { strings } from '~/translations' import useErrors from './useErrors' +import useTranslation from './useTranslation' export const useToasts = () => { + const { t } = useTranslation() const dispatch = useDispatch() const activeToast = useSelector(getActiveToast) const { showErrorReporting } = useErrors() @@ -32,10 +33,10 @@ export const useToasts = () => { //TODO: don't pass title and message, but rather get them based on the SW error code const scheduleErrorWarning = (error: Error, config?: Partial) => scheduleWarning({ - title: strings.ERROR_TOAST_TITLE, - message: strings.ERROR_TOAST_MSG, + title: t('Toasts.errorWarningTitle'), + message: t('Toasts.errorWarningMsg'), interact: { - label: strings.REPORT, + label: t('Toasts.reportBtn'), onInteract: () => { showErrorReporting(error) }, diff --git a/src/hooks/useErrors.ts b/src/hooks/useErrors.ts index eeefc3b66..16774a09f 100644 --- a/src/hooks/useErrors.ts +++ b/src/hooks/useErrors.ts @@ -1,11 +1,15 @@ import { StatusBar } from 'react-native' -import { useErrorContext, ErrorScreens } from '~/errors/errorContext' +import { + useErrorContext, + ErrorScreens, + ErrorDetails, +} from '~/errors/errorContext' const useErrors = () => { const { setError, ...state } = useErrorContext() - const showErrorDisplay = (error?: Error) => { - setError(ErrorScreens.errorDisplay, error ?? null) + const showErrorDisplay = (error: Error, errorDetails?: ErrorDetails) => { + setError(ErrorScreens.errorDisplay, error, errorDetails) } const showErrorReporting = (error?: Error) => { @@ -13,11 +17,11 @@ const useErrors = () => { // does not show up after the modal is visible. Must be shown before the modal is // visible. StatusBar.setHidden(false) - setError(ErrorScreens.errorReporting, error ?? null) + setError(ErrorScreens.errorReporting, error) } const resetError = () => { - setError(null, null) + setError() } return { ...state, showErrorDisplay, showErrorReporting, resetError } diff --git a/src/hooks/useTranslation.ts b/src/hooks/useTranslation.ts index 4959d2622..20848d8c0 100644 --- a/src/hooks/useTranslation.ts +++ b/src/hooks/useTranslation.ts @@ -2,15 +2,17 @@ import { useTranslation as useI18n } from 'react-i18next' import { Locales, localesArr } from '~/translations' import { StorageKeys } from './sdk' import { Agent } from '@jolocom/sdk' +import { useDispatch } from 'react-redux' +import { setCurrentLanguage } from '~/modules/account/actions' const useTranslation = () => { const { i18n, t } = useI18n() + const dispatch = useDispatch() const currentLanguage = i18n.language - const storeLanguage = async (lang: Locales, agent: Agent) => { - return agent.storage.store.setting(StorageKeys.language, { id: lang }) - } + const storeLanguage = async (lang: Locales, agent: Agent) => + agent.storage.store.setting(StorageKeys.language, { id: lang }) const getStoredLanguage = async (agent: Agent) => { const lang = await agent.storage.get.setting(StorageKeys.language) @@ -28,10 +30,12 @@ const useTranslation = () => { } const changeLanguage = async (language: Locales, agent: Agent) => { - if (!localesArr.includes(language)) + if (!localesArr.includes(language)) { throw new Error('Language not supported!') + } i18n.changeLanguage(language) + dispatch(setCurrentLanguage(language)) storeLanguage(language, agent) } diff --git a/src/middleware/records/recordAssembler.ts b/src/middleware/records/recordAssembler.ts index b0d94f701..63d6ffa24 100644 --- a/src/middleware/records/recordAssembler.ts +++ b/src/middleware/records/recordAssembler.ts @@ -1,5 +1,11 @@ -import { IRecordDetails, IRecordStatus, IRecordSteps } from '~/types/records' -import { IRecordConfig } from '~/config/records' +import { + IRecordDetails, + IRecordStatus, + IRecordSteps, + IRecordConfig, + IFlowRecordConfig, + IStatusRecordConfig, +} from '~/types/records' import { FlowType } from '@jolocom/sdk' import { InteractionType } from 'jolocom-lib/js/interactionTokens/types' import truncateDid from '~/utils/truncateDid' @@ -13,7 +19,6 @@ import { import { getCredentialType } from '~/utils/dataMapping' import { capitalizeWord } from '~/utils/stringUtils' import { FlowState } from '@jolocom/sdk/js/interactionManager/flow' -import { strings } from '~/translations' interface IRecordAssembler { messageTypes: string[] @@ -21,11 +26,12 @@ interface IRecordAssembler { summary: InteractionSummary lastMessageDate: number expirationDate: number - config: Partial> + config: IRecordConfig } export class RecordAssembler { - private config: IRecordConfig | undefined + private config: IFlowRecordConfig | undefined + private statusConfig: IStatusRecordConfig private messageTypes: string[] private flowType: FlowType private summary: InteractionSummary @@ -48,7 +54,8 @@ export class RecordAssembler { this.summary = summary this.expirationDate = expirationDate this.lastMessageDate = lastMessageDate - this.config = config[flowType] + this.config = config.flows[flowType] + this.statusConfig = config.status this.status = this.processStatus() this.steps = this.processSteps() } @@ -64,7 +71,7 @@ export class RecordAssembler { } private getTitle(): string { - return this.config?.title ?? 'Unknown' + return this.config?.title ?? this.statusConfig.unknown } private processStatus(): IRecordStatus { @@ -111,14 +118,18 @@ export class RecordAssembler { private appendUnfinishedStep(steps: IRecordSteps[]) { steps.push({ - title: this.config?.steps.unfinished[steps.length] ?? 'Unknown', + title: + this.config?.steps.unfinished[steps.length] ?? + this.statusConfig.unknown, description: - this.status === IRecordStatus.expired ? 'Expired' : 'Pending', + this.status === IRecordStatus.expired + ? this.statusConfig.expired + : this.statusConfig.pending, }) } private getFinishedStepTitle(index: number) { - return this.config?.steps.finished[index] ?? 'Unknown' + return this.config?.steps.finished[index] ?? this.statusConfig.unknown } private assembleCredentialOfferSteps() { @@ -139,7 +150,7 @@ export class RecordAssembler { .map((s) => s.credential?.name?.length ? s.credential?.name - : strings.UNKNOWN, + : this.statusConfig.unknown, ) .join(', '), } @@ -147,7 +158,7 @@ export class RecordAssembler { return { title: this.getFinishedStepTitle(i), description: state.issued - .map((c) => (c.name.length ? c.name : strings.UNKNOWN)) + .map((c) => (c.name.length ? c.name : this.statusConfig.unknown)) .join(', '), } default: @@ -167,7 +178,9 @@ export class RecordAssembler { const displayCreds = areCredsSupplied ? state.providedCredentials[0].suppliedCredentials - .map((c) => (!!c.name.length ? c.name : strings.UNKNOWN)) + .map((c) => + !!c.name.length ? c.name : this.statusConfig.unknown, + ) .join(', ') : requestedCreds @@ -196,7 +209,9 @@ export class RecordAssembler { case 'AuthorizationResponse': return { title: this.getFinishedStepTitle(i), - description: capitalizeWord(state.action ?? 'Authorize'), + description: capitalizeWord( + state.action ?? this.statusConfig.unknown, + ), } default: throw new Error('Wrong interaction type for flow') @@ -225,7 +240,7 @@ export class RecordAssembler { private assembleUnknownSteps() { return this.assembleAllSteps((_, i) => ({ title: this.getFinishedStepTitle(i), - description: 'Unknown', + description: this.statusConfig.unknown, })) } diff --git a/src/modules/account/actions.ts b/src/modules/account/actions.ts index 187def6e9..5ca7129da 100644 --- a/src/modules/account/actions.ts +++ b/src/modules/account/actions.ts @@ -1,3 +1,4 @@ +import { Locales } from '~/translations' import createAction from '~/utils/createAction' import { AccountActions } from './types' @@ -14,3 +15,6 @@ export const setAppLocked = createAction(AccountActions.setAppLocked) export const setScreenHeight = createAction( AccountActions.setScreenHeight, ) +export const setCurrentLanguage = createAction( + AccountActions.setCurrentLanguage, +) diff --git a/src/modules/account/reducers.ts b/src/modules/account/reducers.ts index 09509c87f..cf4dce1f0 100644 --- a/src/modules/account/reducers.ts +++ b/src/modules/account/reducers.ts @@ -1,5 +1,6 @@ import { AccountState, AccountActions } from './types' import { Action } from '~/types/actions' +import { Locales } from '~/translations' const initialState: AccountState = { did: '', @@ -8,6 +9,7 @@ const initialState: AccountState = { showTermsConsent: false, isAppLocked: true, screenHeight: 0, + currentLanguage: Locales.en, } const reducer = ( @@ -29,6 +31,8 @@ const reducer = ( return { ...state, isAppLocked: action.payload } case AccountActions.setScreenHeight: return { ...state, screenHeight: action.payload } + case AccountActions.setCurrentLanguage: + return { ...state, currentLanguage: action.payload } default: return state } diff --git a/src/modules/account/selectors.ts b/src/modules/account/selectors.ts index 3447ec74d..c19715b41 100644 --- a/src/modules/account/selectors.ts +++ b/src/modules/account/selectors.ts @@ -9,3 +9,5 @@ export const shouldShowTermsConsent = (state: RootReducerI) => export const getIsAppLocked = (state: RootReducerI) => state.account.isAppLocked export const getScreenHeight = (state: RootReducerI) => state.account.screenHeight +export const getCurrentLanguage = (state: RootReducerI) => + state.account.currentLanguage diff --git a/src/modules/account/types.ts b/src/modules/account/types.ts index 3837270ec..cc2ca7bc4 100644 --- a/src/modules/account/types.ts +++ b/src/modules/account/types.ts @@ -1,3 +1,5 @@ +import { Locales } from '~/translations' + export enum AccountActions { setDid = 'setDid', setLogged = 'setLogged', @@ -6,6 +8,7 @@ export enum AccountActions { showTermsConsent = 'showTermsConsent', setAppLocked = 'setAppLocked', setScreenHeight = 'setScreenHeight', + setCurrentLanguage = 'setCurrentLanguage', } export interface AccountState { @@ -15,4 +18,5 @@ export interface AccountState { isAppLocked: boolean showTermsConsent: boolean screenHeight: number + currentLanguage: Locales } diff --git a/src/modules/interaction/reducer.ts b/src/modules/interaction/reducer.ts index 4944f91e7..61b418ad0 100644 --- a/src/modules/interaction/reducer.ts +++ b/src/modules/interaction/reducer.ts @@ -4,7 +4,7 @@ import { isCredShareDetails, isCredOfferDetails } from './guards' import { AttrActions, AttributePayload } from '../attributes/types' const initialState: InteractionState = { - details: { flowType: null, id: null }, + details: { flowType: null, id: null, counterparty: null }, } const reducer = ( diff --git a/src/modules/interaction/selectors.ts b/src/modules/interaction/selectors.ts index 8a5a0eeb8..d32513ec1 100644 --- a/src/modules/interaction/selectors.ts +++ b/src/modules/interaction/selectors.ts @@ -28,8 +28,8 @@ import { import { categorizedCredentials } from '~/utils/categoriedCredentials' import { getObjectFirstValue } from '~/utils/objectUtils' import { AttributeI } from '../attributes/types' -import truncateDid from '~/utils/truncateDid' import { attributeConfig } from '~/config/claims' +import { getCounterpartyName } from '~/utils/dataMapping' const makeInteractionSelector = ( guard: (details: InteractionDetails) => details is T, @@ -89,8 +89,7 @@ export const getInteractionCounterparty = createSelector( */ export const getInteractionCounterpartyName = createSelector( [getInteractionCounterparty], - (counterparty) => - counterparty.publicProfile?.name ?? truncateDid(counterparty.did), + getCounterpartyName, ) /** diff --git a/src/modules/loader/reducers.ts b/src/modules/loader/reducers.ts index adc3783cb..1c2221895 100644 --- a/src/modules/loader/reducers.ts +++ b/src/modules/loader/reducers.ts @@ -1,5 +1,3 @@ -import { strings } from '~/translations/strings' - import { LoaderActions, LoaderState, LoaderTypes } from './types' type Actions = { @@ -9,7 +7,7 @@ type Actions = { const initialState: LoaderState = { type: LoaderTypes.default, - msg: strings.EMPTY, + msg: '', isVisible: false, } diff --git a/src/screens/LoggedIn/Documents/CredentialDetails.tsx b/src/screens/LoggedIn/Documents/CredentialDetails.tsx index 3088ac68d..2f935033e 100644 --- a/src/screens/LoggedIn/Documents/CredentialDetails.tsx +++ b/src/screens/LoggedIn/Documents/CredentialDetails.tsx @@ -80,7 +80,10 @@ const CredentialDetails = () => { diff --git a/src/screens/LoggedIn/Documents/DocumentList.tsx b/src/screens/LoggedIn/Documents/DocumentList.tsx new file mode 100644 index 000000000..ffcb5e0f2 --- /dev/null +++ b/src/screens/LoggedIn/Documents/DocumentList.tsx @@ -0,0 +1,319 @@ +import React, { useEffect, useMemo, useState, useLayoutEffect } from 'react' +import { useSelector } from 'react-redux' +import { ScrollView, StyleSheet, View } from 'react-native' +import { useRoute, RouteProp } from '@react-navigation/native' +import { TFunction } from 'i18next' +import { DisplayVal } from '@jolocom/sdk/js/credentials' + +import ScreenContainer from '~/components/ScreenContainer' +import { useTabs } from '~/components/Tabs/context' +import { + getCustomCredentialsByCategoriesByType, + getCustomCredentialsByCategoriesByIssuer, +} from '~/modules/credentials/selectors' +import { + CredentialCategories, + DisplayCredentialDocument, + DisplayCredentialOther, + CredentialsByType, + CredentialsByIssuer, + CredentialsByCategory, + CredentialUITypes, +} from '~/types/credentials' +import ScreenPlaceholder from '~/components/ScreenPlaceholder' +import AdoptedCarousel from '~/components/AdoptedCarousel' +import { MainTabsParamList } from '../MainTabs' +import { ScreenNames } from '~/types/screens' +import JoloText from '~/components/JoloText' +import { JoloTextSizes } from '~/utils/fonts' +import { Colors } from '~/utils/colors' +import BP from '~/utils/breakpoints' +import useTranslation from '~/hooks/useTranslation' +import { uiTypesTerms } from '~/hooks/signedCredentials/utils' +import { + useCredentialOptionalFields, + useDeleteCredential, +} from '~/hooks/credentials' +import { useToasts } from '~/hooks/toasts' +import { usePopupMenu } from '~/hooks/popupMenu' +import DocumentSectionDocumentCard from '~/components/Cards/DocumentSectionCards/DocumentSectionDocumentCard' +import DocumentSectionOtherCard from '~/components/Cards/DocumentSectionCards/DocumentSectionOtherCard' + +const getCredentialDisplayType = (displayType: string, t: TFunction) => { + /** + * - if value is defined + * and it isn't not a pattern: use it as a type; + * - if value is empty make it unknown + */ + const uiType: string | undefined = + uiTypesTerms[displayType as CredentialUITypes] + + const credentialUIType = uiType + ? t(uiType) + : displayType === '' + ? t(uiTypesTerms[CredentialUITypes.unknown]) + : displayType + return credentialUIType +} + +const useHandleMorePress = () => { + const { t } = useTranslation() + const { scheduleErrorWarning } = useToasts() + + const { showPopup } = usePopupMenu() + const deleteCredential = useDeleteCredential() + const handleDelete = async (id: string) => { + try { + await deleteCredential(id) + } catch (e) { + scheduleErrorWarning(e) + } + } + + return ( + id: string, + credentialName: string, + fields: Array>, + photo?: string, + ) => { + const popupOptions = [ + { + title: t('Documents.infoCardOption'), + navigation: { + screen: ScreenNames.CredentialDetails, + params: { + fields, + photo, + title: credentialName, + }, + }, + }, + { + title: t('Documents.deleteCardOption'), + navigation: { + screen: ScreenNames.DragToConfirm, + params: { + title: `${t('Documents.deleteDocumentHeader', { + documentName: credentialName, + })}`, + cancelText: t('Documents.cancelCardOption'), + instructionText: t('Documents.deleteCredentialInstruction'), + onComplete: () => handleDelete(id), + }, + }, + }, + ] + showPopup(popupOptions) + } +} + +const CardList: React.FC = ({ children }) => ( + + {children} + +) + +export const DocumentList = () => { + const { t } = useTranslation() + const [categories, setCategories] = + useState< + | CredentialsByCategory< + CredentialsByType + > + | CredentialsByCategory< + CredentialsByIssuer< + DisplayCredentialDocument | DisplayCredentialOther + > + > + | null + >(null) + const { activeTab, activeSubtab, setActiveTab, tabs } = useTabs() + const route = useRoute>() + const { getOptionalFields } = useCredentialOptionalFields() + + const categoriesByType = useSelector(getCustomCredentialsByCategoriesByType) + const categoriesByIssuer = useSelector( + getCustomCredentialsByCategoriesByIssuer, + ) + + // NOTE: changing the active tab when the navigation params changed + useLayoutEffect(() => { + const newTabId = route.params.initialTab ?? CredentialCategories.document + setActiveTab(tabs.find((t) => t.id === newTabId)!) + }, [route]) + + useEffect(() => { + if (activeSubtab?.id === 'type') { + setCategories(categoriesByType) + } else if (activeSubtab?.id === 'issuer') { + setCategories(categoriesByIssuer) + } + }, [ + activeSubtab?.id, + JSON.stringify(categoriesByType), + JSON.stringify(categoriesByIssuer), + ]) + + const documents = useMemo( + () => + categories !== null ? categories[CredentialCategories.document] : [], + [JSON.stringify(categories)], + ) + const other = useMemo( + () => (categories !== null ? categories[CredentialCategories.other] : []), + [JSON.stringify(categories)], + ) + + const onHandleMore = useHandleMorePress() + + if (categories === null) return null + return ( + <> + + {!documents.length ? ( + + ) : ( + + {documents.map((d) => { + const { credentials, value } = d as + | CredentialsByType + | CredentialsByIssuer + + const credentialUIType = getCredentialDisplayType(value, t) + return ( + + + + {`${credentialUIType} • ${credentials.length}`} + + + ( + + onHandleMore( + c.id, + c.name, + [ + { + key: 'subjectName', + label: t('Documents.subjectNameField'), + value: c.holderName || t('General.anonymous'), + }, + ...getOptionalFields(c), + ], + c.photo, + ) + } + /> + )} + /> + + ) + })} + + )} + + + {!other.length ? ( + + ) : ( + + {other.map((o) => { + const { credentials, value } = o as + | CredentialsByType + | CredentialsByIssuer + const credentialUIType = getCredentialDisplayType(value, t) + + return ( + + + + {`${credentialUIType} • ${credentials.length}`} + + + + { + const fields = getOptionalFields(c) + return ( + + onHandleMore(c.id, c.name, fields, c.photo) + } + /> + ) + }} + /> + + ) + })} + + )} + + + ) +} + +const styles = StyleSheet.create({ + sectionContainer: { + paddingBottom: 22, + }, +}) diff --git a/src/screens/LoggedIn/Documents/DocumentTabs.tsx b/src/screens/LoggedIn/Documents/DocumentTabs.tsx deleted file mode 100644 index fc5418b30..000000000 --- a/src/screens/LoggedIn/Documents/DocumentTabs.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react' - -import { strings } from '~/translations' -import TabsContainer from '~/components/Tabs/Container' -import { Tabs } from '~/components/Tabs/Tabs' -import { IWithCustomStyle } from '~/components/Card/types' -import { CredentialCategories } from '~/types/credentials' -import ScreenContainer from '~/components/ScreenContainer' - -export const documentTabs = [ - { id: CredentialCategories.document, value: strings.DOCUMENTS }, - { id: CredentialCategories.other, value: strings.OTHER }, -] - -export const documentSubtabs = [ - { id: 'type', value: strings.TYPE }, - { id: 'issuer', value: strings.ISSUER }, -] - -const DocumentTabs: React.FC = ({ children }) => { - return ( - - - - {documentTabs.map((t) => ( - - ))} - - - - - - {documentSubtabs.map((st) => ( - - ))} - - - - {() => children} - - ) -} - -export default DocumentTabs diff --git a/src/screens/LoggedIn/Documents/index.tsx b/src/screens/LoggedIn/Documents/index.tsx index 9816586e5..dee38bcdd 100644 --- a/src/screens/LoggedIn/Documents/index.tsx +++ b/src/screens/LoggedIn/Documents/index.tsx @@ -1,233 +1,24 @@ -import React, { useEffect, useMemo, useState, useLayoutEffect } from 'react' -import { useSelector } from 'react-redux' -import { ScrollView, View } from 'react-native' -import { useRoute, RouteProp } from '@react-navigation/native' +import React from 'react' import ScreenContainer from '~/components/ScreenContainer' -import DocumentCard from '~/components/Card/DocumentCard' -import { useTabs } from '~/components/Tabs/context' -import { - getCustomCredentialsByCategoriesByType, - getCustomCredentialsByCategoriesByIssuer, -} from '~/modules/credentials/selectors' -import DocumentTabs, { - documentTabs, -} from '~/screens/LoggedIn/Documents/DocumentTabs' -import OtherCard from '~/components/Card/OtherCard' -import { - CredentialCategories, - DocumentFields, - DisplayCredentialDocument, - DisplayCredentialOther, - CredentialsByType, - CredentialsByIssuer, - CredentialsByCategory, -} from '~/types/credentials' -import ScreenPlaceholder from '~/components/ScreenPlaceholder' -import { strings } from '~/translations' -import { getOptionalFields } from './utils' -import AdoptedCarousel from '~/components/AdoptedCarousel' -import { MainTabsParamList } from '../MainTabs' -import { ScreenNames } from '~/types/screens' -import JoloText from '~/components/JoloText' -import { JoloTextSizes } from '~/utils/fonts' -import { Colors } from '~/utils/colors' -import BP from '~/utils/breakpoints' +import { CredentialCategories } from '~/types/credentials' +import Tabs from '~/components/Tabs/Tabs' +import TabsContainer from '~/components/Tabs/Container' +import { DocumentList } from './DocumentList' +import useTranslation from '~/hooks/useTranslation' -const CardList: React.FC = ({ children }) => { - return ( - - {children} - - ) -} - -const DocumentList = () => { - const [categories, setCategories] = - useState< - | CredentialsByCategory< - CredentialsByType - > - | CredentialsByCategory< - CredentialsByIssuer< - DisplayCredentialDocument | DisplayCredentialOther - > - > - | null - >(null) - const { activeTab, activeSubtab, setActiveTab } = useTabs() - const route = useRoute>() - - const categoriesByType = useSelector(getCustomCredentialsByCategoriesByType) - const categoriesByIssuer = useSelector( - getCustomCredentialsByCategoriesByIssuer, - ) - - // NOTE: changing the active tab when the navigation params changed - useLayoutEffect(() => { - const newTabId = route.params.initialTab ?? CredentialCategories.document - setActiveTab(documentTabs.find((t) => t.id === newTabId)!) - }, [route]) - - useEffect(() => { - if (activeSubtab?.id === 'type') { - setCategories(categoriesByType) - } else if (activeSubtab?.id === 'issuer') { - setCategories(categoriesByIssuer) - } - }, [ - activeSubtab?.id, - JSON.stringify(categoriesByType), - JSON.stringify(categoriesByIssuer), - ]) - - const documents = useMemo( - () => - categories !== null ? categories[CredentialCategories.document] : [], - [JSON.stringify(categories)], - ) - const other = useMemo( - () => (categories !== null ? categories[CredentialCategories.other] : []), - [JSON.stringify(categories)], - ) - - if (categories === null) return null - return ( - <> - - {!documents.length ? ( - - ) : ( - - {documents.map((d) => { - const { credentials, value } = d as - | CredentialsByType - | CredentialsByIssuer - return ( - <> - - - {`${value} • ${credentials.length}`} - - - ( - - )} - /> - - ) - })} - - )} - - - {!other.length ? ( - - ) : ( - - {other.map((o) => { - const { credentials, value } = o as - | CredentialsByType - | CredentialsByIssuer - return ( - <> - - - {`${value} • ${credentials.length}`} - - +const Documents: React.FC = () => { + const { t } = useTranslation() + const tabs = [ + { id: CredentialCategories.document, value: t('Documents.documentsTab') }, + { id: CredentialCategories.other, value: t('Documents.othersTab') }, + ] - ( - - )} - /> - - ) - })} - - )} - - - ) -} + const subtabs = [ + { id: 'type', value: t('Documents.typeSubtab') }, + { id: 'issuer', value: t('Documents.issuerSubtab') }, + ] -const Documents: React.FC = () => { return ( { paddingHorizontal: 0, }} > - - - + + + + {tabs.map((t) => ( + + ))} + + + + + + {subtabs.map((st) => ( + + ))} + + + + {() => } + ) } diff --git a/src/screens/LoggedIn/Documents/utils.ts b/src/screens/LoggedIn/Documents/utils.ts deleted file mode 100644 index bec57fe4e..000000000 --- a/src/screens/LoggedIn/Documents/utils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { strings } from '~/translations' -import moment from 'moment' - -import { ClaimKeys, DisplayCredential } from '~/types/credentials' - -export const filteredOptionalFields = [ - ClaimKeys.familyName, - ClaimKeys.givenName, - ClaimKeys.id, - ClaimKeys.photo, -] - -export const getOptionalFields = ( - credential: T, -) => { - const additionalFields = [ - { - label: strings.ISSUED, - value: moment(credential.issued).format('DD.MM.YYYY'), - }, - { - label: strings.ISSUER, - value: credential.issuer.publicProfile?.name ?? credential.issuer.did, - }, - { - label: strings.EXPIRES, - value: moment(credential.expires).format('DD.MM.YYYY'), - }, - ] - if (!credential.properties.length) return additionalFields - return credential.properties - .filter((p) => !filteredOptionalFields.includes(p.key as ClaimKeys)) - .map(({ label, value }) => ({ - label, - value, - })) - .concat(additionalFields) -} diff --git a/src/screens/LoggedIn/History/RecordHeader.tsx b/src/screens/LoggedIn/History/RecordHeader.tsx index 036ea91b0..0ee332b12 100644 --- a/src/screens/LoggedIn/History/RecordHeader.tsx +++ b/src/screens/LoggedIn/History/RecordHeader.tsx @@ -2,16 +2,22 @@ import React from 'react' import { IRecordHeader } from './types' import { useRecord } from './context' import ScreenContainer from '~/components/ScreenContainer' -import { strings } from '~/translations' +import { useTranslation } from 'react-i18next' const RecordHeader: React.FC = ({ title, testID = 'record-header', }) => { + const { t } = useTranslation() const { activeSection } = useRecord() return ( - {title || (Object.values(activeSection)[0] as string) || strings.HISTORY} + {t( + // @ts-expect-error + title || + (Object.values(activeSection)[0] as string) || + 'BottomBar.history', + )} ) } diff --git a/src/screens/LoggedIn/History/RecordItemsList.tsx b/src/screens/LoggedIn/History/RecordItemsList.tsx index ca38e0b69..45cc1a07a 100644 --- a/src/screens/LoggedIn/History/RecordItemsList.tsx +++ b/src/screens/LoggedIn/History/RecordItemsList.tsx @@ -16,12 +16,13 @@ import { IHistorySectionData } from '~/types/records' import { useRecord } from './context' import RecordItem from './components/RecordItem' import ScreenPlaceholder from '~/components/ScreenPlaceholder' -import { strings } from '~/translations' import RecordHeader from './RecordHeader' +import useTranslation from '~/hooks/useTranslation' const ITEMS_PER_PAGE = 5 const RecordItemsList: React.FC = ({ id, flows }) => { + const { t } = useTranslation() const sectionListRef = useRef(null) const { updateActiveSection } = useRecord() @@ -172,10 +173,8 @@ const RecordItemsList: React.FC = ({ id, flows }) => { /> ) : ( ) } diff --git a/src/screens/LoggedIn/History/components/RecordFinalStep.tsx b/src/screens/LoggedIn/History/components/RecordFinalStep.tsx index 816530d7d..42c3e8a13 100644 --- a/src/screens/LoggedIn/History/components/RecordFinalStep.tsx +++ b/src/screens/LoggedIn/History/components/RecordFinalStep.tsx @@ -6,7 +6,6 @@ import { Colors } from '~/utils/colors' import JoloText from '~/components/JoloText' import { JoloTextSizes } from '~/utils/fonts' import { SuccessTick, ErrorIcon } from '~/assets/svg' -import { strings } from '~/translations' import useTranslation from '~/hooks/useTranslation' const RecordFinalStep: React.FC = ({ @@ -45,7 +44,7 @@ const RecordFinalStep: React.FC = ({ customStyles={{ textAlign: 'left' }} numberOfLines={1} > - {description || t(strings.UNKNOWN)} + {description || t('General.unknown')} diff --git a/src/screens/LoggedIn/History/components/RecordItem.tsx b/src/screens/LoggedIn/History/components/RecordItem.tsx index 3dcc64519..bed38a5ef 100644 --- a/src/screens/LoggedIn/History/components/RecordItem.tsx +++ b/src/screens/LoggedIn/History/components/RecordItem.tsx @@ -6,10 +6,13 @@ import { IRecordDetails } from '~/types/records' import { IRecordItemProps } from '../types' import RecordItemHeader from './RecordItemHeader' import RecordItemDetails from './RecordItemDetails' +import { useSelector } from 'react-redux' +import { getCurrentLanguage } from '~/modules/account/selectors' const RecordItem: React.FC = React.memo( ({ id, onDropdown, isFocused }) => { const [itemDetails, setItemDetails] = useState(null) + const currentLanguage = useSelector(getCurrentLanguage) const { assembleInteractionDetails } = useHistory() @@ -37,7 +40,7 @@ const RecordItem: React.FC = React.memo( // scheduleErrorWarning(e) }) - }, []) + }, [currentLanguage]) return ( = React.memo( ) }, - (prevProps, nextProps) => { - return ( - prevProps.isFocused === nextProps.isFocused && - prevProps.lastUpdated === nextProps.lastUpdated && - JSON.stringify(prevProps.onDropdown) === - JSON.stringify(nextProps.onDropdown) - ) - }, + (prevProps, nextProps) => + prevProps.isFocused === nextProps.isFocused && + prevProps.lastUpdated === nextProps.lastUpdated && + JSON.stringify(prevProps.onDropdown) === + JSON.stringify(nextProps.onDropdown), ) export default RecordItem diff --git a/src/screens/LoggedIn/History/components/RecordItemHeader.tsx b/src/screens/LoggedIn/History/components/RecordItemHeader.tsx index 821e3dd2f..fe3ac57cb 100644 --- a/src/screens/LoggedIn/History/components/RecordItemHeader.tsx +++ b/src/screens/LoggedIn/History/components/RecordItemHeader.tsx @@ -27,6 +27,11 @@ const RecordItemHeader: React.FC<{ details: IRecordDetails | null }> = ({ ignoreScaling kind={JoloTextKind.title} size={JoloTextSizes.mini} + numberOfLines={1} + customStyles={{ + flex: 1, + textAlign: 'left', + }} > {details ? details.title : '███████'} @@ -36,7 +41,11 @@ const RecordItemHeader: React.FC<{ details: IRecordDetails | null }> = ({ testID="record-item-time" size={JoloTextSizes.mini} color={Colors.white} - customStyles={{ alignSelf: 'center', marginRight: 16 }} + customStyles={{ + alignSelf: 'center', + marginRight: 16, + marginLeft: 8, + }} > {details ? details.time : '██'} diff --git a/src/screens/LoggedIn/History/components/RecordStep.tsx b/src/screens/LoggedIn/History/components/RecordStep.tsx index 331f6ddb2..b540f408d 100644 --- a/src/screens/LoggedIn/History/components/RecordStep.tsx +++ b/src/screens/LoggedIn/History/components/RecordStep.tsx @@ -5,7 +5,6 @@ import { IRecordSteps } from '~/types/records' import { Colors } from '~/utils/colors' import JoloText, { JoloTextKind } from '~/components/JoloText' import { JoloTextSizes } from '~/utils/fonts' -import { strings } from '~/translations' import useTranslation from '~/hooks/useTranslation' const RecordStep: React.FC = ({ title, description }) => { @@ -34,7 +33,7 @@ const RecordStep: React.FC = ({ title, description }) => { customStyles={{ textAlign: 'left' }} numberOfLines={1} > - {description || t(strings.UNKNOWN)} + {description || t('General.unknown')} diff --git a/src/screens/LoggedIn/History/index.tsx b/src/screens/LoggedIn/History/index.tsx index d5d514508..8ce886523 100644 --- a/src/screens/LoggedIn/History/index.tsx +++ b/src/screens/LoggedIn/History/index.tsx @@ -4,15 +4,9 @@ import React from 'react' import ScreenContainer from '~/components/ScreenContainer' import TabsContainer from '~/components/Tabs/Container' import Tabs from '~/components/Tabs/Tabs' -import { strings } from '~/translations' +import useTranslation from '~/hooks/useTranslation' import Record from './Record' -const SUBTABS = [ - { id: 'all', value: strings.ALL }, - { id: 'shared', value: strings.SHARED }, - { id: 'received', value: strings.RECEIVED }, -] - export enum RecordTypes { all = 'all', shared = 'shared', @@ -20,6 +14,13 @@ export enum RecordTypes { } const History = () => { + const { t } = useTranslation() + const SUBTABS = [ + { id: 'all', value: t('History.allTab') }, + { id: 'shared', value: t('History.sharedTab') }, + { id: 'received', value: t('History.receivedTab') }, + ] + return ( { - return attributeConfig -} +const getAttributeConfigPrimitive = (): TPrimitiveAttributesConfig => + attributeConfig const primitiveAttributesConfig = getAttributeConfigPrimitive() const IdentityCredentials = () => { + const { t } = useTranslation() const redirect = useRedirect() const attributes = useSelector(getAttributes) @@ -47,13 +47,11 @@ const IdentityCredentials = () => { const primitiveAttributesWithValues = Object.entries( primitiveAttributesConfig, - ).map(([type, config]) => { - return { - type: type as PrimitiveAttributeTypes, - label: config.label, - values: attributes[type as PrimitiveAttributeTypes] ?? [], - } - }) + ).map(([type, config]) => ({ + type: type as PrimitiveAttributeTypes, + label: config.label, + values: attributes[type as PrimitiveAttributeTypes] ?? [], + })) const isPrimitiveAttributesEmpty = primitiveAttributesWithValues.every( (a) => !a.values.length, @@ -62,31 +60,37 @@ const IdentityCredentials = () => { return ( - {strings.YOUR_INFO_IS_QUITE_EMPTY} + {t('Identity.attributesMissingInfo')} {primitiveAttributesWithValues.map(({ type, label, values }) => { + const isEmpty = !values.length const concatValues = getAttributeValueBasedOnConfig(type, values).map( (value) => concatValuesIdentity(type, value), ) const hideCreateNew = type === AttributeTypes.name && concatValues.length > 0 return ( - + redirect(ScreenNames.CredentialForm, { type })} > - + {/* @ts-expect-error @TERMS */} + {!hideCreateNew && } - {concatValues.length ? ( + {!isEmpty ? ( concatValues.map((field) => ( handleDeleteCredentialSI(field.id, type)} /> )) diff --git a/src/screens/LoggedIn/Identity/IdentityIntro.tsx b/src/screens/LoggedIn/Identity/IdentityIntro.tsx index f98fb33e5..2704d60d9 100644 --- a/src/screens/LoggedIn/Identity/IdentityIntro.tsx +++ b/src/screens/LoggedIn/Identity/IdentityIntro.tsx @@ -4,12 +4,12 @@ import Fallin from '~/components/animation/Fallin' import Btn, { BtnTypes } from '~/components/Btn' import JoloText, { JoloTextKind } from '~/components/JoloText' -import { strings } from '~/translations' import { Colors } from '~/utils/colors' import BP from '~/utils/breakpoints' import SingleCredentialWizard from './SingleCredentialWizard' import { IdentityTabIds } from './types' +import useTranslation from '~/hooks/useTranslation' enum IdentityForms { SingleCredential = 'SingleCredential', @@ -21,6 +21,7 @@ interface Props { const WelcomeSheet: React.FC = ({ onSubmit }) => { const [isTopSheetVisible, setTopSheetVisibility] = useState(true) const [activeForm, setActiveForm] = useState(null) + const { t } = useTranslation() const animateSheet = () => LayoutAnimation.configureNext({ @@ -91,7 +92,7 @@ const WelcomeSheet: React.FC = ({ onSubmit }) => { }), }} > - {strings.IT_IS_TIME_TO_CREATE} + {t('Identity.widgetWelcome')} = ({ onSubmit }) => { customContainerStyles={{ backgroundColor: Colors.mainBlack }} testID="single-credential-button" > - {strings.START_NOW} + {t('Identity.widgetStartBtn')} )} diff --git a/src/screens/LoggedIn/Identity/SingleCredentialWizard.tsx b/src/screens/LoggedIn/Identity/SingleCredentialWizard.tsx index 7ee383e97..acb015d8e 100644 --- a/src/screens/LoggedIn/Identity/SingleCredentialWizard.tsx +++ b/src/screens/LoggedIn/Identity/SingleCredentialWizard.tsx @@ -2,25 +2,26 @@ import React from 'react' import { View } from 'react-native' import { attributeConfig } from '~/config/claims' import { useSICActions } from '~/hooks/attributes' -import { strings } from '~/translations' import { AttributeTypes } from '~/types/credentials' import Wizard from '~/components/Wizard' import { nameValidation } from '~/config/validation' import { trimObjectValues } from '~/utils/stringUtils' - -const WIZARD_CONFIG = { - 0: { - label: strings.WHAT_IS_YOUR_NAME, - form: attributeConfig[AttributeTypes.name], - submitLabel: strings.CREATE, - validationSchema: nameValidation, - }, -} +import useTranslation from '~/hooks/useTranslation' const SingleCredentialWizard: React.FC<{ onFormSubmit: () => void }> = ({ onFormSubmit, }) => { const { handleCreateCredentialSI } = useSICActions() + const { t } = useTranslation() + + const WIZARD_CONFIG = { + 0: { + label: t('Identity.widgetNameHeader'), + form: attributeConfig[AttributeTypes.name], + submitLabel: t('Identity.widgetConfirmBtn'), + validationSchema: nameValidation, + }, + } const handleSubmit = async (fields: Record) => { fields = trimObjectValues(fields) diff --git a/src/screens/LoggedIn/Identity/index.tsx b/src/screens/LoggedIn/Identity/index.tsx index ac724cef4..17b3fd001 100644 --- a/src/screens/LoggedIn/Identity/index.tsx +++ b/src/screens/LoggedIn/Identity/index.tsx @@ -6,10 +6,11 @@ import { getAttributes } from '~/modules/attributes/selectors' import { useSelector } from 'react-redux' import IdentityCredentials from './IdentityCredentials' import IdentityTabs from './tabs' -import { strings } from '~/translations' import { IdentityTabIds } from './types' +import useTranslation from '~/hooks/useTranslation' const Identity = () => { + const { t } = useTranslation() const attributes = useSelector(getAttributes) const showIdentityIntro = !Boolean(Object.keys(attributes).length) const [initialTab, setInitialTab] = useState(IdentityTabIds.credentials) @@ -32,7 +33,7 @@ const Identity = () => { customStyles={{ paddingHorizontal: 0 }} > - {strings.YOUR_INFO} + {t('Identity.header')} diff --git a/src/screens/LoggedIn/Identity/tabs/types.ts b/src/screens/LoggedIn/Identity/tabs/types.ts index 3897e5866..b074824c2 100644 --- a/src/screens/LoggedIn/Identity/tabs/types.ts +++ b/src/screens/LoggedIn/Identity/tabs/types.ts @@ -1,4 +1,4 @@ -import { IWithCustomStyle } from '~/components/Card/types' +import { IWithCustomStyle } from '~/types/props' export interface ITabsContext { activeTab: string | undefined diff --git a/src/screens/LoggedIn/Main.tsx b/src/screens/LoggedIn/Main.tsx index 9825cd942..ddab62211 100644 --- a/src/screens/LoggedIn/Main.tsx +++ b/src/screens/LoggedIn/Main.tsx @@ -24,9 +24,7 @@ import TermsConsent from '~/screens/Modals/TermsConsent' import MainTabs from './MainTabs' import CredentialForm from '../Modals/Forms/CredentialForm' import { PrimitiveAttributeTypes } from '~/types/credentials' -import { IField } from '~/components/Card/types' import CredentialDetails from './Documents/CredentialDetails' -import InteractionTest from './Settings/Development/InteractionCardsTest' import PinRecoveryInstructions from '../Modals/PinRecoveryInstructions' import Recovery from '../Modals/Recovery' import { @@ -37,26 +35,27 @@ import { } from '~/utils/screenSettings' import PopupMenu, { PopupMenuProps } from '~/components/PopupMenu' import CollapsibleClone from './Settings/Development/CollapsibleClone' +import InteractionPasteTest from './Settings/Development/InteractionPasteTest' +import { Colors } from '~/utils/colors' +import { IField } from '~/types/props' export type TransparentModalsParamsList = { [ScreenNames.PopupMenu]: PopupMenuProps } const TransparentModalsStack = createStackNavigator() -const TransparentModals = () => { - return ( - - - - ) -} +const TransparentModals = () => ( + + + +) export type MainStackParamList = { [ScreenNames.Interaction]: undefined @@ -79,13 +78,13 @@ export type MainStackParamList = { photo?: string } // DEV + [ScreenNames.InteractionPasteTest]: undefined [ScreenNames.ButtonsTest]: undefined [ScreenNames.CollapsibleTest]: undefined [ScreenNames.LoaderTest]: undefined [ScreenNames.NotificationsTest]: undefined [ScreenNames.InputTest]: undefined [ScreenNames.PasscodeTest]: undefined - [ScreenNames.InteractionCardsTest]: undefined [ScreenNames.PinRecoveryInstructions]: undefined [ScreenNames.PasscodeRecovery]: { isAccessRestore: boolean @@ -170,6 +169,11 @@ const Main: React.FC = () => { {__DEV__ && ( <> + { component={PasscodeTest} options={screenTransitionSlideFromRight} /> - )} {/* Settings Screens -> End */} @@ -212,7 +211,17 @@ const Main: React.FC = () => { () -const MainTabs = () => ( - { - return - }} - > - - - - - -) +const MainTabs = () => { + const { t } = useTranslation() + + return ( + { + return + }} + > + + + + + + ) +} export default MainTabs diff --git a/src/screens/LoggedIn/Settings/About.tsx b/src/screens/LoggedIn/Settings/About.tsx index 6983c4ed9..c8b417d2b 100644 --- a/src/screens/LoggedIn/Settings/About.tsx +++ b/src/screens/LoggedIn/Settings/About.tsx @@ -10,9 +10,10 @@ import { useRedirectTo } from '~/hooks/navigation' import { ScreenNames } from '~/types/screens' // @ts-ignore import packageJson from '~/../package.json' -import { strings } from '~/translations/strings' +import useTranslation from '~/hooks/useTranslation' const About = () => { + const { t } = useTranslation() const redirectToTerms = useRedirectTo(ScreenNames.TermsOfService) const redirectToPrivacyPolicy = useRedirectTo(ScreenNames.PrivacyPolicy) @@ -28,7 +29,7 @@ const About = () => { kind={JoloTextKind.subtitle} size={JoloTextSizes.middle} > - {`You are running version ${version}`} + {t('About.versionInfo', { version })} { - {strings.TERMS_OF_SERVICE} + {t('Terms of Service.header')} - {strings.PRIVACY_POLICY} + {t('PrivacyPolicy.header')} diff --git a/src/screens/LoggedIn/Settings/BackupIdentity.tsx b/src/screens/LoggedIn/Settings/BackupIdentity.tsx index 3d48165e6..0b4d4897c 100644 --- a/src/screens/LoggedIn/Settings/BackupIdentity.tsx +++ b/src/screens/LoggedIn/Settings/BackupIdentity.tsx @@ -1,17 +1,17 @@ import React from 'react' -import { ScrollView, View } from 'react-native' +import { View } from 'react-native' import JoloText, { JoloTextKind } from '~/components/JoloText' import ScreenContainer from '~/components/ScreenContainer' import { JoloTextSizes } from '~/utils/fonts' import Section from './components/Section' -import { strings } from '~/translations' import Block from '~/components/Block' import Btn, { BtnTypes } from '~/components/Btn' import { Colors } from '~/utils/colors' import BP from '~/utils/breakpoints' import Collapsible from '~/components/Collapsible' import NavigationHeader, { NavHeaderType } from '~/components/NavigationHeader' +import useTranslation from '~/hooks/useTranslation' const BackupBlock: React.FC<{ title: string @@ -48,6 +48,7 @@ const BackupBlock: React.FC<{ ) const BackupIdentity = () => { + const { t } = useTranslation() // FIXME: add proper values const lastBackup = '18.07.2020' @@ -56,7 +57,7 @@ const BackupIdentity = () => { - {strings.BACKUP_OPTIONS} + {t('BackupOptions.header')} @@ -69,19 +70,19 @@ const BackupIdentity = () => { }} > - {strings.BACKUP_OPTIONS} + {t('BackupOptions.header')} {}} /> {}} /> @@ -92,7 +93,9 @@ const BackupIdentity = () => { paddingTop: 20, }} > - {strings.LAST_BACKUP} + + {t('BackupOptions.lastBackupInfo')} + {lastBackup} diff --git a/src/screens/LoggedIn/Settings/ChangePin.tsx b/src/screens/LoggedIn/Settings/ChangePin.tsx index 648f6aebb..7fb35806a 100644 --- a/src/screens/LoggedIn/Settings/ChangePin.tsx +++ b/src/screens/LoggedIn/Settings/ChangePin.tsx @@ -3,7 +3,6 @@ import Keychain from 'react-native-keychain' import ScreenContainer from '~/components/ScreenContainer' -import { strings } from '~/translations/strings' import { PIN_SERVICE, PIN_USERNAME } from '~/utils/keychainConsts' import { sleep } from '~/utils/generic' @@ -12,6 +11,8 @@ import { useGoBack } from '~/hooks/navigation' import Passcode from '~/components/Passcode' import { useLoader } from '~/hooks/loader' import { useEffect } from 'react' +import useTranslation from '~/hooks/useTranslation' +import { useToasts } from '~/hooks/toasts' enum PasscodeState { verify = 'verify', @@ -19,31 +20,31 @@ enum PasscodeState { repeat = 'repeat', } -const DEFAULT_ERROR = strings.WRONG_PASSCODE - const ChangePin: React.FC = () => { + const { t } = useTranslation() const loader = useLoader() const { keychainPin } = useGetStoredAuthValues() const goBack = useGoBack() + const { scheduleErrorWarning } = useToasts() const [passcodeState, setPasscodeState] = useState( PasscodeState.verify, ) const [newPin, setNewPin] = useState('') - const [errorTitle, setErrorTitle] = useState(DEFAULT_ERROR) + const [errorTitle, setErrorTitle] = useState('') useEffect(() => { - setErrorTitle(DEFAULT_ERROR) + setErrorTitle(t('ChangePasscode.wrongCodeHeader')) }, [newPin]) const headerTitle = () => { switch (passcodeState) { case PasscodeState.verify: - return strings.CURRENT_PASSCODE + return t('ChangePasscode.currentHeader') case PasscodeState.create: - return strings.CREATE_NEW_PASSCODE + return t('CreatePasscode.createHeader') case PasscodeState.repeat: - return strings.VERIFY_PASSCODE + return t('VerifyPasscode.verifyHeader') } } @@ -55,10 +56,12 @@ const ChangePin: React.FC = () => { storage: Keychain.STORAGE_TYPE.AES, }) }, - { success: strings.PASSCODE_CHANGED }, + { success: t('ChangePasscode.successHeader') }, (error) => { if (error) { - //TODO: possibility to show toast? + scheduleErrorWarning(error, { + message: t('Toasts.failedStoreMsg'), + }) setPasscodeState(PasscodeState.verify) } else { goBack() @@ -78,7 +81,7 @@ const ChangePin: React.FC = () => { setNewPin(pin) handleStateChange(PasscodeState.repeat) } else { - setErrorTitle('The same as old') + setErrorTitle(t('ChangePasscode.sameCodeHeader')) throw new Error() } } diff --git a/src/screens/LoggedIn/Settings/ContactUs.tsx b/src/screens/LoggedIn/Settings/ContactUs.tsx index fa0c3b66d..96f86f5f9 100644 --- a/src/screens/LoggedIn/Settings/ContactUs.tsx +++ b/src/screens/LoggedIn/Settings/ContactUs.tsx @@ -7,7 +7,6 @@ import { IOption } from '~/components/Selectable' import Btn, { BtnTypes } from '~/components/Btn' import JoloKeyboardAwareScroll from '~/components/JoloKeyboardAwareScroll' -import { strings } from '~/translations/strings' import { JoloTextSizes } from '~/utils/fonts' import { InputValidation, regexValidations } from '~/utils/stringUtils' import { Colors } from '~/utils/colors' @@ -21,16 +20,10 @@ import { InputValidityState } from '~/components/Input/types' import { useAssertConnection } from '~/hooks/connection' import { useAdjustResizeInputMode } from '~/hooks/generic' import { useSafeArea } from 'react-native-safe-area-context' - -const INQUIRIES_LIST = [ - strings.POSSIBLE_PARTNERSHIP, - strings.ISSUES_WITH_THE_APP, - strings.I_LOST_MY_WALLET, - strings.HOW_TO_BECOME_PART_OF_THE_PROJECT, - strings.OTHER, -] +import useTranslation from '~/hooks/useTranslation' const ContactUs: React.FC = () => { + const { t } = useTranslation() const navigateBack = useGoBack() const showSuccess = useSuccess() const { sendContactReport } = useSentry() @@ -43,6 +36,14 @@ const ContactUs: React.FC = () => { useAdjustResizeInputMode() useAssertConnection() + const INQUIRIES_LIST = [ + t('ContactUs.issueOption_1'), + t('ContactUs.issueOption_2'), + t('ContactUs.issueOption_3'), + t('ContactUs.issueOption_4'), + t('ContactUs.issueOption_5'), + ] + const options = useMemo( () => INQUIRIES_LIST.map((el) => ({ id: el.split(' ').join(''), value: el })), @@ -89,21 +90,23 @@ const ContactUs: React.FC = () => { keyboardShouldPersistTaps="handled" >
- - {strings.WHAT_WE_ARE_GOING_TO_TALK_ABOUT} - - + {t('ContactUs.issueHeader')} +
- {strings.ANYTHING_SPECIFIC_TO_MENTION} + {t('ContactUs.suggestionHeader')} - {strings.DARE_TO_SUGGEST_SMTH} + {t('ContactUs.suggestionSubheader')} {({ focusInput }) => ( @@ -119,7 +122,7 @@ const ContactUs: React.FC = () => {
- {strings.WANT_TO_GET_IN_TOUCH} + {t('ContactUs.contactHeader')} {({ focusInput }) => ( @@ -127,7 +130,7 @@ const ContactUs: React.FC = () => { validation={regexValidations[InputValidation.email]} value={contactValue} updateInput={setContactValue} - placeholder={strings.CONTACT_US_GET_IN_TOUCH} + placeholder={t('ContactUs.contactPlaceholder')} onValidation={handleContactValidation} onFocus={focusInput} /> @@ -140,8 +143,8 @@ const ContactUs: React.FC = () => { customStyles={{ textAlign: 'left', marginTop: 12 }} > {contactValid - ? strings.WE_DO_NOT_STORE_DATA - : strings.PLEASE_ENTER_A_VALID_EMAIL} + ? t('ContactUs.contactInputInfo') + : t('ErrorReporting.contactInputError')}
{ onPress={handleSubmit} disabled={!isBtnEnabled()} > - {strings.SEND} + {t('ContactUs.submitBtn')} diff --git a/src/screens/LoggedIn/Settings/Development/InteractionCardsTest.tsx b/src/screens/LoggedIn/Settings/Development/InteractionCardsTest.tsx deleted file mode 100644 index 14943123f..000000000 --- a/src/screens/LoggedIn/Settings/Development/InteractionCardsTest.tsx +++ /dev/null @@ -1,397 +0,0 @@ -import React from 'react'; -import { ScrollView, View } from 'react-native'; -import ScreenContainer from '~/components/ScreenContainer'; -import IncomingOfferDoc from '~/screens/Modals/Interaction/InteractionFlow/components/card/offer/document'; -import IncomingOfferOther from '~/screens/Modals/Interaction/InteractionFlow/components/card/offer/other'; -import { IncomingRequestDoc } from '~/screens/Modals/Interaction/InteractionFlow/components/card/request/document'; -import { IncomingRequestOther } from '~/screens/Modals/Interaction/InteractionFlow/components/card/request/other'; -import Section from '../components/Section'; - -const REQUEST_DOCS = [ - { - id: 0, - holderName: 'Jane Fitzgerald', // TODO: we won't receive it in the same format from sdk, this field should be added - name: 'Digital Passport', - properties: [ - { - key: 'a', - label: 'Date of birth', - value: '04.06.1984' - }, - { - key: 'b', - label: 'Expiry date', - value: '04.06.1984' - }, - ], - image: 'https://i.pinimg.com/564x/63/9d/5b/639d5b86c73addfaeeb103ef0eb61041.jpg', - }, - { - id: 1, - holderName: 'Jane Fransis Scott Adelina Fitzgerald', // TODO: we won't receive it in the same format from sdk, this field should be added - // holderName: 'Jane Fitzgerald', // TODO: we won't receive it in the same format from sdk, this field should be added - name: 'Digital Passport', - properties: [ - { - key: 'a', - label: 'Date of birth lorem impsum Date of birth lorem impsum Date of birth lorem impsum ', - value: '04.06.1984' - }, - { - key: 'b', - label: 'Expiry date', - value: '04.06.1984' - }, - { - key: 'c', - label: 'Issue date', - value: '04.06.1984' - } - - ], - image: 'https://i.pinimg.com/564x/63/9d/5b/639d5b86c73addfaeeb103ef0eb61041.jpg', - highlight: 'SPECI2014' // TODO: we won't receive it in the same format from sdk, this field should be added - }, - { - id: 2, - holderName: 'Jane Fransis Scott Adelina Fitzgerald', // TODO: we won't receive it in the same format from sdk, this field should be added - name: 'Friendly document name', - properties: [ - { - key: 'a', - label: 'Description of the input', - value: 'Some more info that can fit' - }, - { - key: 'b', - label: 'Extra long description of the input', - value: 'Some more info that can fit asjdasdjs in here and if it is not going on sjdsjd' - }, - ], - image: 'https://i.pinimg.com/564x/63/9d/5b/639d5b86c73addfaeeb103ef0eb61041.jpg', - }, - { - id: 3, - holderName: 'Jane Fransis Scott Adelina Fitzgerald', // TODO: we won't receive it in the same format from sdk, this field should be added - name: 'Friendly document name', - properties: [ - { - key: 'a', - label: 'Description of the input', - value: 'Some more info that can fit asjdasdjs fit in here and if it is not going on sjdsjd' - }, - { - key: 'b', - label: 'Extra long description of the input', - value: 'Some more info that can fit asjdasdjs fit in here and if it is not going on sjdsjd' - }, - ], - }, - { - id: 4, - holderName: 'Jane Fransis Scott Adelina Fitzgerald', // TODO: we won't receive it in the same format from sdk, this field should be added - name: 'Digital Passport', - properties: [ - { - key: 'a', - label: 'Description of the input', - value: 'Some more info that can fit in here and if it is not going on sjdsjd' - }, - { - key: 'b', - label: 'Extra long description of the input', - value: 'Some more info that can fit asjdasdjs' - }, - ], - image: 'https://i.pinimg.com/564x/63/9d/5b/639d5b86c73addfaeeb103ef0eb61041.jpg', - highlight: 'SPECI2014', - }, - { - id: 5, - // holderName: 'Jane Fransis Scott Adelina Fitzgerald', // TODO: we won't receive it in the same format from sdk, this field should be added - holderName: 'Jane Fitzgerald', // TODO: we won't receive it in the same format from sdk, this field should be added - name: 'Digital Passport', - properties: [], - }, - { - id: 6, - name: 'Digital Passport', - properties: [], - } - -] - -const REQUEST_OTHER = [ - { - id: 0, - name: 'Concert ticket', - title: 'Concert ticket', - subtitle: 'Tame Impala 2023', - properties: [ - { - key: 'a', - label: 'Place', - value: 'Berlin Arena' - }, - { - key: 'b', - label: 'Date', - value: '04.07.2023' - }, - { - key: 'c', - label: 'Seat', - value: '15a, sector D' - }, - { - key: 'c', - label: 'Seat', - value: '15a, sector D' - }, - - ], - }, - { - id: 1, - name: 'Concert ticket', - title: 'Concert ticket', - subtitle: 'Tame Impala 2023 Wolrd tour', - properties: [ - { - key: 'a', - label: 'Description of the input', - value: 'Information that should be previewed here' - }, - { - key: 'b', - label: 'Description of the input', - value: 'Information' - }, - { - key: 'b', - label: 'Description of the input', - value: 'Information' - }, - - ], - }, - { - id: 2, - name: 'Concert ticket', - title: 'Concert ticket', - subtitle: 'Tame Impala 2023 Wolrd tour', - properties: [ - { - key: 'a', - label: 'Description of the input', - value: 'Information that should be previewed here' - }, - { - key: 'b', - label: 'Description of the input', - value: 'Information that should be previewed here' - }, - { - key: 'b', - label: 'Description of the input', - value: 'Information that should be previewed here' - }, - - ], - }, - { - id: 3, - name: 'Concert ticket', - title: 'Concert ticket', - subtitle: 'Tame Impala 2023 Wolrd tour', - properties: [], - }, -] - -const OFFER_DOCS = [ - { - id: 0, - name: 'Digital Passport', - title: 'Digital Passport', - properties: [ - { - key: 'a', - label: 'Name', - value: '04.06.1984' - }, - { - key: 'b', - label: 'Date of birth', - value: '04.06.1984' - }, - { - key: 'c', - label: 'Expiry date', - value: '04.06.1984' - }, - { - key: 'd', - label: 'Something else', - value: '04.06.1984' - }, - - ], - }, - { - id: 1, - name: 'Friendly document name', - title: 'Friendly document name', - properties: [ - { - key: 'a', - label: 'Name', - value: '04.06.1984' - }, - { - key: 'b', - label: 'Date of birth', - value: '04.06.1984' - }, - { - key: 'c', - label: 'Expiry date', - value: '04.06.1984' - }, - - ], - }, - { - id: 2, - name: 'Friendly document name', - title: 'Friendly document name', - properties: [], - }, -] - -const OFFER_OTHER = [ - { - id: 0, - name: 'Concert ticket', - title: 'Concert ticket', - properties: [ - { - key: 'a', - label: 'Name', - value: '04.06.1984' - }, - { - key: 'b', - label: 'Date of birth', - value: '04.06.1984' - }, - { - key: 'c', - label: 'Expiry date', - value: '04.06.1984' - }, - { - key: 'd', - label: 'Something else', - value: '04.06.1984' - }, - - ], - }, - { - id: 1, - name: 'Concert ticket', - title: 'Concert ticket', - properties: [ - { - key: 'a', - label: 'Name', - value: '04.06.1984' - }, - { - key: 'b', - label: 'Date of birth', - value: '04.06.1984' - }, - { - key: 'c', - label: 'Expiry date', - value: '04.06.1984' - }, - - ], - }, - { - id: 2, - name: 'Concert ticket', - title: 'Concert ticket', - properties: [], - }, - -] - - -const InteractionTest = () => { - return ( - - - - Incoming request - documents - {REQUEST_DOCS.map(c => ( - - - - - - ))} - Incoming request - others - {REQUEST_OTHER.map(c => ( - - - - - ))} - - Incoming offer - documents - - {OFFER_DOCS.map(c => ( - - - - - ))} - - Incoming offer - other - - {OFFER_OTHER.map(c => ( - - - - - ))} - - - - ) -} - -export default InteractionTest; \ No newline at end of file diff --git a/src/screens/LoggedIn/Settings/Development/InteractionPasteTest.tsx b/src/screens/LoggedIn/Settings/Development/InteractionPasteTest.tsx new file mode 100644 index 000000000..caf4647fc --- /dev/null +++ b/src/screens/LoggedIn/Settings/Development/InteractionPasteTest.tsx @@ -0,0 +1,66 @@ +import { useNavigation } from '@react-navigation/native' +import React, { useEffect, useState } from 'react' +import { SDKError } from 'react-native-jolocom' +import Btn from '~/components/Btn' +import Input from '~/components/Input' +import JoloText, { JoloTextKind } from '~/components/JoloText' +import ScreenContainer from '~/components/ScreenContainer' +import { useInteractionStart } from '~/hooks/interactions/handlers' +import { ScreenNames } from '~/types/screens' +import { Colors } from '~/utils/colors' + +const DEFAULT_TOKEN = '' + +const InteractionPasteTest = () => { + const [token, setToken] = useState(DEFAULT_TOKEN) + const [error, setError] = useState('') + + const navigation = useNavigation() + + const startInteraction = useInteractionStart() + + const handleTokenSubmit = async () => { + if (!token) setError('Please paste interaction token') + try { + await startInteraction(token) + setToken(DEFAULT_TOKEN) + navigation.navigate(ScreenNames.Interaction) + } catch (e) { + if (e instanceof SyntaxError) { + setError(SDKError.codes.ParseJWTFailed) + } else if (e.message === 'Token expired') { + setError(SDKError.codes.TokenExpired) + } else { + setError(SDKError.codes.Unknown) + } + console.warn({ e }) + } + } + + useEffect(() => { + setError('') + }, [token]) + + return ( + + + {error ? ( + + ) : null} + Process + + ) +} + +export default InteractionPasteTest diff --git a/src/screens/LoggedIn/Settings/Development/index.tsx b/src/screens/LoggedIn/Settings/Development/index.tsx index 3d73af54d..32a0e4077 100644 --- a/src/screens/LoggedIn/Settings/Development/index.tsx +++ b/src/screens/LoggedIn/Settings/Development/index.tsx @@ -17,9 +17,7 @@ const DevelopmentSection = () => { const redirectToNotifications = useRedirectTo(ScreenNames.NotificationsTest) const redirectToInputs = useRedirectTo(ScreenNames.InputTest) const redirectToPasscode = useRedirectTo(ScreenNames.PasscodeTest) - const redirectToInteractionCards = useRedirectTo( - ScreenNames.InteractionCardsTest, - ) + const redirect = useRedirect() const { showPopup } = usePopupMenu() @@ -31,54 +29,67 @@ const DevelopmentSection = () => { } return ( -
- Development - - - - - - - - - - - - -
+ <> +
+ [DEV] Interactions + + + +
+
+ [DEV] Error handling + + + +
+ +
+ [DEV] UI component + + + + + + + + + + +
+ ) } diff --git a/src/screens/LoggedIn/Settings/EnableBiometryOption.tsx b/src/screens/LoggedIn/Settings/EnableBiometryOption.tsx index 679f16e32..d7fafd8c7 100644 --- a/src/screens/LoggedIn/Settings/EnableBiometryOption.tsx +++ b/src/screens/LoggedIn/Settings/EnableBiometryOption.tsx @@ -1,16 +1,16 @@ import React, { useEffect, useState, useCallback } from 'react' import { View } from 'react-native' import { BiometryType } from 'react-native-biometrics' -import { useDispatch } from 'react-redux' import ToggleSwitch from '~/components/ToggleSwitch' import { useBiometry } from '~/hooks/biometry' import { useDisableLock } from '~/hooks/generic' import { useToasts } from '~/hooks/toasts' -import { strings } from '~/translations/strings' +import useTranslation from '~/hooks/useTranslation' import Option from './components/Option' const EnableBiometryOption = () => { + const { t } = useTranslation() /* State to define of this component is displayed: depends on if any biometrics were enrolled */ const [isOptionVisible, setIsOptionVisible] = useState(false) /* On state that is controlled and passed to ToggleSwitch */ @@ -19,8 +19,6 @@ const EnableBiometryOption = () => { const [enrolledBiometry, setEnrolledBiometry] = useState(undefined) - const dispatch = useDispatch() - const { resetBiometry, getBiometry, @@ -28,7 +26,7 @@ const EnableBiometryOption = () => { authenticate, getEnrolledBiometry, } = useBiometry() - const { scheduleWarning } = useToasts() + const { scheduleErrorWarning } = useToasts() const disableLock = useDisableLock() /* check if we should display this component or not */ @@ -85,17 +83,17 @@ const EnableBiometryOption = () => { } } catch (e) { setIsOn((prevState) => !prevState) - scheduleWarning({ - title: strings.WHOOPS, - message: isOn ? strings.COULDNOT_DEACTIVATE : strings.COULDNOT_ACTIVATE, - }) + scheduleErrorWarning(e) } } if (!isOptionVisible) return null return (