From 10e5b21ff6ddbf6915985fe9004c4246424382ca Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 11 Sep 2025 12:06:38 -0700 Subject: [PATCH 01/34] feat: added getCardProcessForCardType utility function Signed-off-by: Mac Deluca --- .../non-photo/EvidenceTypeListScreen.tsx | 4 +-- app/src/bcsc-theme/utils/card-utils.ts | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 app/src/bcsc-theme/utils/card-utils.ts diff --git a/app/src/bcsc-theme/features/verify/non-photo/EvidenceTypeListScreen.tsx b/app/src/bcsc-theme/features/verify/non-photo/EvidenceTypeListScreen.tsx index fdf83e993..99d310d75 100644 --- a/app/src/bcsc-theme/features/verify/non-photo/EvidenceTypeListScreen.tsx +++ b/app/src/bcsc-theme/features/verify/non-photo/EvidenceTypeListScreen.tsx @@ -11,6 +11,7 @@ import { BCDispatchAction, BCState } from '@/store' import { BCSCCardType } from '@/bcsc-theme/types/cards' import { BCSCCardProcess } from '@/bcsc-theme/api/hooks/useAuthorizationApi' import { useTranslation } from 'react-i18next' +import { getCardProcessForCardType } from '@/bcsc-theme/utils/card-utils' type EvidenceTypeListScreenProps = { navigation: StackNavigationProp @@ -105,8 +106,7 @@ const EvidenceTypeListScreen: React.FC = ({ navigat // filter data based on the selected card type (process) let cards: Record = {} - const selectedProcess = - store.bcsc.cardType === BCSCCardType.NonPhoto ? BCSCCardProcess.BCSCNonPhoto : BCSCCardProcess.NonBCSC + const selectedProcess = getCardProcessForCardType(store.bcsc.cardType) data.processes.forEach((p) => { // only show card that matches the selected process if (p.process === selectedProcess) { diff --git a/app/src/bcsc-theme/utils/card-utils.ts b/app/src/bcsc-theme/utils/card-utils.ts new file mode 100644 index 000000000..0091ce5f4 --- /dev/null +++ b/app/src/bcsc-theme/utils/card-utils.ts @@ -0,0 +1,26 @@ +import { BCSCCardProcess } from '../api/hooks/useAuthorizationApi' +import { BCSCCardType } from '../types/cards' + +/** + * Get the card process for a given card type. + * + * @throws {Error} If the card type is invalid or None. + * @param {BCSCCardType} cardType - The type of BCSC card. + * @returns {*} {BCSCCardProcess} The corresponding card process. + */ +export function getCardProcessForCardType(cardType: BCSCCardType): BCSCCardProcess { + switch (cardType) { + case BCSCCardType.Combined: + return BCSCCardProcess.BCSC + case BCSCCardType.Photo: + return BCSCCardProcess.BCSC + case BCSCCardType.NonPhoto: + return BCSCCardProcess.BCSCNonPhoto + case BCSCCardType.Other: + return BCSCCardProcess.NonBCSC + case BCSCCardType.None: + throw new Error(`Invalid card type: ${BCSCCardType.None}}`) + default: + throw new Error(`Unknown card type: ${cardType}`) + } +} From e000a6d0842767cda50c52bd836370aff452c53d Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 11 Sep 2025 14:27:51 -0700 Subject: [PATCH 02/34] feat: added useDebounce hook and wired up with service list Signed-off-by: Mac Deluca --- .../bcsc-theme/features/services/Services.tsx | 94 +++++++++++++++++-- app/src/hooks/useDebounce.ts | 22 +++++ 2 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 app/src/hooks/useDebounce.ts diff --git a/app/src/bcsc-theme/features/services/Services.tsx b/app/src/bcsc-theme/features/services/Services.tsx index 2000470a7..91b15e515 100644 --- a/app/src/bcsc-theme/features/services/Services.tsx +++ b/app/src/bcsc-theme/features/services/Services.tsx @@ -1,16 +1,34 @@ -import React from 'react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' import TabScreenWrapper from '@/bcsc-theme/components/TabScreenWrapper' -import { ThemedText, useTheme } from '@bifold/core' -import { StyleSheet } from 'react-native' - +import { ThemedText, TOKENS, useServices, useStore, useTheme } from '@bifold/core' +import { Alert, StyleSheet, TextInput } from 'react-native' import ServiceButton from './components/ServiceButton' -import { mockServices } from '@bcsc-theme/fixtures/services' +import useApi from '@/bcsc-theme/api/hooks/useApi' +import useDataLoader from '@/bcsc-theme/hooks/useDataLoader' +import { ClientMetadata } from '@/bcsc-theme/api/hooks/useMetadataApi' +import { BCState } from '@/store' +import { getCardProcessForCardType } from '@/bcsc-theme/utils/card-utils' +import { useDebounce } from '@/hooks/useDebounce' +import { useNavigation } from '@react-navigation/native' // to be replaced with API response or translation entries, whichever ends up being the case const mockHeaderText = 'Browse websites you can log in to with this app' +/** + * Services screen component that displays a list of services accessible with the user's BCSC card. + * + * @return {*} {JSX.Element} The Services screen component. + */ const Services: React.FC = () => { const { Spacing } = useTheme() + const { metadata } = useApi() + const [store] = useStore() + const navigation = useNavigation() + const [logger] = useServices([TOKENS.UTIL_LOGGER]) + + const [searchText, setSearchText] = useState('') + const debouncedSearchText = useDebounce(searchText, 300) + const styles = StyleSheet.create({ headerText: { paddingHorizontal: Spacing.md, @@ -18,17 +36,73 @@ const Services: React.FC = () => { }, }) + const { + data: services, + load, + isReady, + } = useDataLoader(() => metadata.getClientMetadata(), { + onError: (error) => { + logger.error('Error loading services', error as Error) + }, + }) + + useEffect(() => { + load() + }, [load]) + + // Filter services based on the user's card type and search text + const filteredServices = useMemo(() => { + if (!services) { + return [] + } + + // Filter services based on the user's card type (ie: card process) + const supportedServices = services.filter((service) => + service.allowed_identification_processes.includes(getCardProcessForCardType(store.bcsc.cardType)) + ) + + // Return all supported services when there's no search text + if (debouncedSearchText.trim() === '') { + return supportedServices + } + + // Filter supported services based on the search text (case insensitive) + const query = debouncedSearchText.toLowerCase() + return supportedServices.filter((service) => service.client_name.toLowerCase().includes(query)) + }, [services, debouncedSearchText, store.bcsc.cardType]) + + // Alert the user if services fail to load + if (!services && isReady) { + Alert.alert('Failed to load services', 'Please try again later.', [ + { + text: 'OK', + onPress: () => { + navigation.goBack() + }, + }, + ]) + } + return ( {mockHeaderText} - {mockServices.map((service) => ( + { + setSearchText(event.nativeEvent.text) + }} + /> + {filteredServices.map((service) => ( { + // TODO (MD): implement service press action + }} /> ))} diff --git a/app/src/hooks/useDebounce.ts b/app/src/hooks/useDebounce.ts new file mode 100644 index 000000000..dca4ae3e6 --- /dev/null +++ b/app/src/hooks/useDebounce.ts @@ -0,0 +1,22 @@ +import { useEffect, useState } from 'react' + +/** + * A custom React hook that debounces a value. + * + * @template T The type of the value to be debounced. + * @param {T} value The value to be debounced. + * @param {number} delayMs The debounce delay in milliseconds. + * @returns {*} {T} The debounced value. + */ +export const useDebounce = (value: T, delayMs: number) => { + const [debouncedValue, setDebouncedValue] = useState(value) + + useEffect(() => { + const debounceHandler = setTimeout(() => setDebouncedValue(value), delayMs) + + // Cleanup timeout if value or delay changes + return () => clearTimeout(debounceHandler) + }, [value, delayMs]) + + return debouncedValue +} From ff02efaaf9e143f452d99eb2c9328f98b2e30db5 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 11 Sep 2025 14:37:59 -0700 Subject: [PATCH 03/34] chore: updated tests for getCardProcessForCardType and updated enum refs Signed-off-by: Mac Deluca --- .../bcsc-theme/utils/card-utils.test.ts | 42 +++++++++++++++++++ .../api/hooks/useAuthorizationApi.tsx | 7 +--- .../non-photo/EvidenceTypeListScreen.tsx | 1 - app/src/bcsc-theme/types/cards.ts | 6 +++ app/src/bcsc-theme/utils/card-utils.ts | 3 +- 5 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 app/__tests__/bcsc-theme/utils/card-utils.test.ts diff --git a/app/__tests__/bcsc-theme/utils/card-utils.test.ts b/app/__tests__/bcsc-theme/utils/card-utils.test.ts new file mode 100644 index 000000000..76c36bf18 --- /dev/null +++ b/app/__tests__/bcsc-theme/utils/card-utils.test.ts @@ -0,0 +1,42 @@ +import { BCSCCardProcess, BCSCCardType } from '@/bcsc-theme/types/cards' +import { getCardProcessForCardType } from '@/bcsc-theme/utils/card-utils' + +describe('Card Utils', () => { + describe('getCardProcessForCardType', () => { + it('should return BCSC for Combined card type', () => { + expect(getCardProcessForCardType(BCSCCardType.Combined)).toBe(BCSCCardProcess.BCSC) + }) + + it('should return BCSC for Photo card type', () => { + expect(getCardProcessForCardType(BCSCCardType.Photo)).toBe(BCSCCardProcess.BCSC) + }) + + it('should return BCSCNonPhoto for NonPhoto card type', () => { + expect(getCardProcessForCardType(BCSCCardType.NonPhoto)).toBe(BCSCCardProcess.BCSCNonPhoto) + }) + + it('should return NonBCSC for Other card type', () => { + expect(getCardProcessForCardType(BCSCCardType.Other)).toBe(BCSCCardProcess.NonBCSC) + }) + + it('should throw an error for None card type', () => { + expect(() => getCardProcessForCardType(BCSCCardType.None)).toThrow(`Invalid card type: ${BCSCCardType.None}}`) + }) + + it('should throw an error for unknown card type', () => { + expect(() => getCardProcessForCardType(99 as any)).toThrow('Unknown card type: 99') + }) + + it('should support all BCSCCardType values', () => { + const cardTypes = Object.values(BCSCCardType) + cardTypes.forEach((cardType) => { + if (cardType === BCSCCardType.None) { + expect(() => getCardProcessForCardType(cardType)).toThrow() + } else { + const process = getCardProcessForCardType(cardType) + expect(Object.values(BCSCCardProcess)).toContain(process) + } + }) + }) + }) +}) diff --git a/app/src/bcsc-theme/api/hooks/useAuthorizationApi.tsx b/app/src/bcsc-theme/api/hooks/useAuthorizationApi.tsx index 911cf3d61..3c402c259 100644 --- a/app/src/bcsc-theme/api/hooks/useAuthorizationApi.tsx +++ b/app/src/bcsc-theme/api/hooks/useAuthorizationApi.tsx @@ -4,15 +4,10 @@ import { withAccount } from './withAccountGuard' import { createDeviceSignedJWT } from 'react-native-bcsc-core' import { isAxiosError } from 'axios' import { ProvinceCode } from '@/bcsc-theme/utils/address-utils' +import { BCSCCardProcess } from '@/bcsc-theme/types/cards' const INVALID_REGISTRATION_REQUEST = 'invalid_registration_request' -export enum BCSCCardProcess { - BCSC = 'IDIM L3 Remote BCSC Photo Identity Verification', - BCSCNonPhoto = 'IDIM L3 Remote BCSC Non-Photo Identity Verification', - NonBCSC = 'IDIM L3 Remote Non-BCSC Identity Verification', -} - export interface VerifyInPersonResponseData { process: BCSCCardProcess user_code: string diff --git a/app/src/bcsc-theme/features/verify/non-photo/EvidenceTypeListScreen.tsx b/app/src/bcsc-theme/features/verify/non-photo/EvidenceTypeListScreen.tsx index 99d310d75..cc5dbabfd 100644 --- a/app/src/bcsc-theme/features/verify/non-photo/EvidenceTypeListScreen.tsx +++ b/app/src/bcsc-theme/features/verify/non-photo/EvidenceTypeListScreen.tsx @@ -9,7 +9,6 @@ import useDataLoader from '@/bcsc-theme/hooks/useDataLoader' import { EvidenceMetadataResponseData, EvidenceType } from '@/bcsc-theme/api/hooks/useEvidenceApi' import { BCDispatchAction, BCState } from '@/store' import { BCSCCardType } from '@/bcsc-theme/types/cards' -import { BCSCCardProcess } from '@/bcsc-theme/api/hooks/useAuthorizationApi' import { useTranslation } from 'react-i18next' import { getCardProcessForCardType } from '@/bcsc-theme/utils/card-utils' diff --git a/app/src/bcsc-theme/types/cards.ts b/app/src/bcsc-theme/types/cards.ts index ebd45cf47..99d95dc17 100644 --- a/app/src/bcsc-theme/types/cards.ts +++ b/app/src/bcsc-theme/types/cards.ts @@ -5,3 +5,9 @@ export enum BCSCCardType { NonPhoto = 'BC Services Card without Photo', Other = 'Other ID(s)', } + +export enum BCSCCardProcess { + BCSC = 'IDIM L3 Remote BCSC Photo Identity Verification', + BCSCNonPhoto = 'IDIM L3 Remote BCSC Non-Photo Identity Verification', + NonBCSC = 'IDIM L3 Remote Non-BCSC Identity Verification', +} diff --git a/app/src/bcsc-theme/utils/card-utils.ts b/app/src/bcsc-theme/utils/card-utils.ts index 0091ce5f4..3bf3665b7 100644 --- a/app/src/bcsc-theme/utils/card-utils.ts +++ b/app/src/bcsc-theme/utils/card-utils.ts @@ -1,5 +1,4 @@ -import { BCSCCardProcess } from '../api/hooks/useAuthorizationApi' -import { BCSCCardType } from '../types/cards' +import { BCSCCardProcess, BCSCCardType } from '../types/cards' /** * Get the card process for a given card type. From ed2eaba01e9e1b97e554902d2b57269d65672400 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 11 Sep 2025 14:40:55 -0700 Subject: [PATCH 04/34] chore: added tests for useDebounce custom hook Signed-off-by: Mac Deluca --- app/__tests__/hooks/useDebounce.test.tsx | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 app/__tests__/hooks/useDebounce.test.tsx diff --git a/app/__tests__/hooks/useDebounce.test.tsx b/app/__tests__/hooks/useDebounce.test.tsx new file mode 100644 index 000000000..ca8a4e9bb --- /dev/null +++ b/app/__tests__/hooks/useDebounce.test.tsx @@ -0,0 +1,71 @@ +import { useDebounce } from '@/hooks/useDebounce' +import { act, renderHook } from '@testing-library/react-native' + +describe('useDebounce', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.runOnlyPendingTimers() + jest.useRealTimers() + }) + + it('should return the initial value immediately', () => { + const { result } = renderHook(() => useDebounce('hello', 500)) + expect(result.current).toBe('hello') + }) + + it('should not update value before delay', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { + initialProps: { value: 'a', delay: 500 }, + }) + + rerender({ value: 'b', delay: 500 }) + // advance less than delay + act(() => { + jest.advanceTimersByTime(400) + }) + + expect(result.current).toBe('a') // still old value + }) + + it('should update value after delay', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { + initialProps: { value: 'a', delay: 500 }, + }) + + rerender({ value: 'b', delay: 500 }) + act(() => { + jest.advanceTimersByTime(500) + }) + + expect(result.current).toBe('b') // updated after debounce delay + }) + + it('should reset timer if value changes before delay', () => { + const { result, rerender } = renderHook(({ value, delay }) => useDebounce(value, delay), { + initialProps: { value: 'a', delay: 500 }, + }) + + rerender({ value: 'b', delay: 500 }) + + act(() => { + jest.advanceTimersByTime(300) // not enough yet + }) + + rerender({ value: 'c', delay: 500 }) + + act(() => { + jest.advanceTimersByTime(300) // still not enough for new value + }) + + expect(result.current).toBe('a') // still initial + + act(() => { + jest.advanceTimersByTime(200) // complete the 500ms for 'c' + }) + + expect(result.current).toBe('c') + }) +}) From 604be31d7acd8ce795a269bb65aaa1b4263bf8d9 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 11 Sep 2025 16:00:03 -0700 Subject: [PATCH 05/34] chore: wired up new ServiceDetailsScreen to navigation Signed-off-by: Mac Deluca --- .../services/ServiceDetailsScreen.tsx | 19 +++++++++++++++++++ .../bcsc-theme/features/services/Services.tsx | 12 ++++++++++-- app/src/bcsc-theme/navigators/MainStack.tsx | 8 ++++++++ app/src/bcsc-theme/navigators/TabStack.tsx | 1 + app/src/bcsc-theme/types/navigators.ts | 3 +++ .../hooks/{useDebounce.ts => useDebounce.tsx} | 2 +- 6 files changed, 42 insertions(+), 3 deletions(-) create mode 100644 app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx rename app/src/hooks/{useDebounce.ts => useDebounce.tsx} (90%) diff --git a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx new file mode 100644 index 000000000..8b08983b6 --- /dev/null +++ b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx @@ -0,0 +1,19 @@ +import { BCSCRootStackParams, BCSCScreens } from '@/bcsc-theme/types/navigators' +import { ThemedText } from '@bifold/core' +import { StackScreenProps } from '@react-navigation/stack' +import { View } from 'react-native' + +type ServiceDetailsScreenProps = StackScreenProps + +/** + * Renders the service details screen component, which displays information about a specific service. + * + * @returns {*} {JSX.Element} The service screen component or null if not implemented. + */ +export const ServiceDetailsScreen: React.FC = (props: ServiceDetailsScreenProps) => { + return ( + + {props.route.params.service.client_name} + + ) +} diff --git a/app/src/bcsc-theme/features/services/Services.tsx b/app/src/bcsc-theme/features/services/Services.tsx index 91b15e515..5b386035b 100644 --- a/app/src/bcsc-theme/features/services/Services.tsx +++ b/app/src/bcsc-theme/features/services/Services.tsx @@ -10,6 +10,10 @@ import { BCState } from '@/store' import { getCardProcessForCardType } from '@/bcsc-theme/utils/card-utils' import { useDebounce } from '@/hooks/useDebounce' import { useNavigation } from '@react-navigation/native' +import { BCSCRootStackParams, BCSCScreens } from '@/bcsc-theme/types/navigators' +import { StackNavigationProp } from '@react-navigation/stack' + +type ServicesNavigationProp = StackNavigationProp // to be replaced with API response or translation entries, whichever ends up being the case const mockHeaderText = 'Browse websites you can log in to with this app' @@ -23,7 +27,7 @@ const Services: React.FC = () => { const { Spacing } = useTheme() const { metadata } = useApi() const [store] = useStore() - const navigation = useNavigation() + const navigation = useNavigation() const [logger] = useServices([TOKENS.UTIL_LOGGER]) const [searchText, setSearchText] = useState('') @@ -83,6 +87,8 @@ const Services: React.FC = () => { ]) } + // TODO (MD): implement a loading UI + return ( @@ -101,7 +107,9 @@ const Services: React.FC = () => { title={service.client_name} description={service.client_description} onPress={() => { - // TODO (MD): implement service press action + navigation.navigate(BCSCScreens.ServiceDetailsScreen, { + service, + }) }} /> ))} diff --git a/app/src/bcsc-theme/navigators/MainStack.tsx b/app/src/bcsc-theme/navigators/MainStack.tsx index cc2e11fe7..cff694c67 100644 --- a/app/src/bcsc-theme/navigators/MainStack.tsx +++ b/app/src/bcsc-theme/navigators/MainStack.tsx @@ -11,6 +11,7 @@ import BCSCTabStack from './TabStack' import createHelpHeaderButton from '../components/HelpHeaderButton' import { HelpCentreUrl } from '@/constants' import { createWebviewHeaderBackButton } from '../components/WebViewBackButton' +import { ServiceDetailsScreen } from '../features/services/ServiceDetailsScreen' const MainStack: React.FC = () => { const { currentStep } = useTour() @@ -66,6 +67,13 @@ const MainStack: React.FC = () => { headerBackTitleVisible: false, })} /> + ({ + headerShown: true, + })} + /> ) diff --git a/app/src/bcsc-theme/navigators/TabStack.tsx b/app/src/bcsc-theme/navigators/TabStack.tsx index 6c8fac247..92eb8ced6 100644 --- a/app/src/bcsc-theme/navigators/TabStack.tsx +++ b/app/src/bcsc-theme/navigators/TabStack.tsx @@ -3,6 +3,7 @@ import React from 'react' import { StyleSheet, Text, useWindowDimensions, View } from 'react-native' import { SafeAreaView } from 'react-native-safe-area-context' import Icon from 'react-native-vector-icons/MaterialCommunityIcons' + import { testIdWithKey, useTheme } from '@bifold/core' import Account from '../features/account/Account' import Home from '../features/home/Home' diff --git a/app/src/bcsc-theme/types/navigators.ts b/app/src/bcsc-theme/types/navigators.ts index 9f42f1809..7424a7f6e 100644 --- a/app/src/bcsc-theme/types/navigators.ts +++ b/app/src/bcsc-theme/types/navigators.ts @@ -1,6 +1,7 @@ import { NavigatorScreenParams } from '@react-navigation/native' import { EvidenceType } from '../api/hooks/useEvidenceApi' import { BCSCCardType } from './cards' +import { ClientMetadata } from '../api/hooks/useMetadataApi' export enum BCSCStacks { TabStack = 'BCSCTabStack', @@ -44,6 +45,7 @@ export enum BCSCScreens { EvidenceIDCollection = 'BCSCEvidenceIDCollection', ResidentialAddressScreen = 'BCSCResidentialAddressScreen', RemoveAccountConfirmation = 'RemoveAccountConfirmationScreen', + ServiceDetailsScreen = 'ServiceDetailsScreen', } export type BCSCTabStackParams = { @@ -59,6 +61,7 @@ export type BCSCRootStackParams = { [BCSCScreens.ManualPairingCode]: undefined [BCSCScreens.PairingConfirmation]: { serviceName: string; serviceId: string } [BCSCScreens.RemoveAccountConfirmation]: undefined + [BCSCScreens.ServiceDetailsScreen]: { service: ClientMetadata } } export type BCSCVerifyIdentityStackParams = { diff --git a/app/src/hooks/useDebounce.ts b/app/src/hooks/useDebounce.tsx similarity index 90% rename from app/src/hooks/useDebounce.ts rename to app/src/hooks/useDebounce.tsx index dca4ae3e6..7c554df82 100644 --- a/app/src/hooks/useDebounce.ts +++ b/app/src/hooks/useDebounce.tsx @@ -8,7 +8,7 @@ import { useEffect, useState } from 'react' * @param {number} delayMs The debounce delay in milliseconds. * @returns {*} {T} The debounced value. */ -export const useDebounce = (value: T, delayMs: number) => { +export const useDebounce = (value: T, delayMs: number) => { const [debouncedValue, setDebouncedValue] = useState(value) useEffect(() => { From 04e7339c5610f2148f4d3d62563b2b3e3b9ffcc4 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 11 Sep 2025 16:14:21 -0700 Subject: [PATCH 06/34] feat: added localizations for Services Signed-off-by: Mac Deluca --- .../services/ServiceDetailsScreen.tsx | 22 ++++++++++++++++--- app/src/localization/en/index.ts | 8 +++++++ app/src/localization/fr/index.ts | 8 +++++++ app/src/localization/pt-br/index.ts | 10 ++++++++- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx index 8b08983b6..d5bb4540b 100644 --- a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx +++ b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx @@ -1,7 +1,8 @@ import { BCSCRootStackParams, BCSCScreens } from '@/bcsc-theme/types/navigators' -import { ThemedText } from '@bifold/core' +import { ThemedText, useTheme } from '@bifold/core' import { StackScreenProps } from '@react-navigation/stack' -import { View } from 'react-native' +import { useTranslation } from 'react-i18next' +import { StyleSheet, View } from 'react-native' type ServiceDetailsScreenProps = StackScreenProps @@ -11,9 +12,24 @@ type ServiceDetailsScreenProps = StackScreenProps = (props: ServiceDetailsScreenProps) => { + const { t } = useTranslation() + const { Spacing } = useTheme() + + const styles = StyleSheet.create({ + screenContainer: { + flex: 1, + padding: Spacing.lg, + }, + }) + return ( - + {props.route.params.service.client_name} + {t('Services.ServiceLoginInstructions')} + {t('Services.ServiceLoginProof')} + {t('Services.ServiceGoto')} + {t('Services.ServicePreferComputer')} + {t('Services.ServicePreferComputerHelp')} ) } diff --git a/app/src/localization/en/index.ts b/app/src/localization/en/index.ts index 97a337c46..b7c8c6bac 100644 --- a/app/src/localization/en/index.ts +++ b/app/src/localization/en/index.ts @@ -215,6 +215,14 @@ const translation = { "WhereToUseLink": "See where you can use BC Wallet", "BadQRCodeDescription": "Ths QR code scanned doesn't work with BC Wallet. BC Wallet only works with participating services.\n\nIt currently can't add digital credentials by taking photos of physical ones." }, + "Services": { + "CatalogueTitle": "Browse websites you can log in to with this app", + "ServiceLoginInstructions": "You will need to go to their website first if you want to log in to it. You can't log in to services directly from this app.", + "ServiceLoginProof": "You will use this app to prove who you are when you log in.", + "ServiceGoto": "Go to", + "ServicePreferComputer": "Prefer to use a computer or tablet?", + "ServicePreferComputerHelp": "On that device go to:" + }, "Unified": { "Steps": { "ScanOrTakePhotos": "Scan or take photos of your ID.", diff --git a/app/src/localization/fr/index.ts b/app/src/localization/fr/index.ts index f1ce18f5e..457813f44 100644 --- a/app/src/localization/fr/index.ts +++ b/app/src/localization/fr/index.ts @@ -212,6 +212,14 @@ const translation = { "WhereToUseLink": "Voyez où vous pouvez utiliser BC Wallet.", "BadQRCodeDescription": "Le code QR scanné ne fonctionne pas avec BC Wallet. BC Wallet ne fonctionne qu'avec les services participants.\n\nIl ne peut actuellement pas ajouter de justificatifs numériques en prenant des photos de justificatifs physiques." }, + "Services": { + "CatalogueTitle": "Browse websites you can log in to with this app (FR)", + "ServiceLoginInstructions": "You will need to go to their website first if you want to log in to it. You can't log in to services directly from this app. (FR)", + "ServiceLoginProof": "You will use this app to prove who you are when you log in. (FR)", + "ServiceGoto": "Go to (FR)", + "ServicePreferComputer": "Prefer to use a computer or tablet? (FR)", + "ServicePreferComputerHelp": "On that device go to: (FR)" + }, "Unified": { "Steps": { "ScanOrTakePhotos": "Scan or take photos of your ID. (FR)", diff --git a/app/src/localization/pt-br/index.ts b/app/src/localization/pt-br/index.ts index 6fce10232..8448e09d4 100644 --- a/app/src/localization/pt-br/index.ts +++ b/app/src/localization/pt-br/index.ts @@ -102,7 +102,7 @@ const translation = { }, "PersonCredential": { "ServicesCardInstalled": "Step 1: BC Services Card app installed (PT-BR)", - "InstallServicesCard": "Step 1: Install the BC Services Card app (FR", + "InstallServicesCard": "Step 1: Install the BC Services Card app (PT-BR", "InstallApp": "Install the app (PT-BR)", "AppOnOtherDevice": "I have it on another device (PT-BR)", "CreatePersonCred": "Step 2: Create your Person credential (PT-BR)", @@ -212,6 +212,14 @@ const translation = { "WhereToUseLink": "See where you can use BC Wallet (PT-BR)", "BadQRCodeDescription": "Ths QR code scanned doesn't work with BC Wallet. BC Wallet only works with participating services.\n\nIt currently can't add digital credentials by taking photos of physical ones. (PT-BR)" }, + "Services": { + "CatalogueTitle": "Browse websites you can log in to with this app (PT-BR)", + "ServiceLoginInstructions": "You will need to go to their website first if you want to log in to it. You can't log in to services directly from this app. (PT-BR)", + "ServiceLoginProof": "You will use this app to prove who you are when you log in. (PT-BR)", + "ServiceGoto": "Go to (PT-BR)", + "ServicePreferComputer": "Prefer to use a computer or tablet? (PT-BR)", + "ServicePreferComputerHelp": "On that device go to: (PT-BR)" + }, "Unified": { "Steps": { "ScanOrTakePhotos": "Scan or take photos of your ID. (PT-BR)", From 6a49b2bd86cf337b3f234ad703a3b3d5d836364c Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 11 Sep 2025 16:48:03 -0700 Subject: [PATCH 07/34] fix: issue with async request blocking navigation Signed-off-by: Mac Deluca --- .../components/WebViewBackButton.tsx | 5 +++-- .../services/ServiceDetailsScreen.tsx | 20 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/src/bcsc-theme/components/WebViewBackButton.tsx b/app/src/bcsc-theme/components/WebViewBackButton.tsx index c3fa824f0..436222977 100644 --- a/app/src/bcsc-theme/components/WebViewBackButton.tsx +++ b/app/src/bcsc-theme/components/WebViewBackButton.tsx @@ -21,6 +21,9 @@ export const createWebviewHeaderBackButton = ( const HeaderLeft = (props: HeaderBackButtonProps) => { const [, dispatch] = useStore() const handleBackPress = async () => { + // navigate back before refreshing to avoid blocking the UI + navigation.goBack() + // Refresh when leaving webviews in case account / device action was taken within the webview if (client.tokens?.refresh_token) { const tokenData = await client.getTokensForRefreshToken(client.tokens?.refresh_token) @@ -32,8 +35,6 @@ export const createWebviewHeaderBackButton = ( }) } } - - navigation.goBack() } return diff --git a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx index d5bb4540b..612237bc3 100644 --- a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx +++ b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx @@ -1,5 +1,6 @@ import { BCSCRootStackParams, BCSCScreens } from '@/bcsc-theme/types/navigators' -import { ThemedText, useTheme } from '@bifold/core' +import { Link, ThemedText, useTheme } from '@bifold/core' +import { useNavigation } from '@react-navigation/native' import { StackScreenProps } from '@react-navigation/stack' import { useTranslation } from 'react-i18next' import { StyleSheet, View } from 'react-native' @@ -12,6 +13,7 @@ type ServiceDetailsScreenProps = StackScreenProps = (props: ServiceDetailsScreenProps) => { + const { service } = props.route.params const { t } = useTranslation() const { Spacing } = useTheme() @@ -19,17 +21,27 @@ export const ServiceDetailsScreen: React.FC = (props: screenContainer: { flex: 1, padding: Spacing.lg, + gap: Spacing.lg, }, }) return ( - {props.route.params.service.client_name} + {service.client_name} {t('Services.ServiceLoginInstructions')} {t('Services.ServiceLoginProof')} - {t('Services.ServiceGoto')} - {t('Services.ServicePreferComputer')} + { + props.navigation.navigate(BCSCScreens.WebView, { + url: service.client_uri, + title: service.client_name, + }) + }} + > + {t('Services.ServicePreferComputer')} {t('Services.ServicePreferComputerHelp')} + {}}> ) } From a2716b0508aaf578e6b612fc608558f73cad2bdd Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Thu, 11 Sep 2025 17:01:15 -0700 Subject: [PATCH 08/34] chore: updated styling for search input Signed-off-by: Mac Deluca --- app/src/bcsc-theme/features/services/Services.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/src/bcsc-theme/features/services/Services.tsx b/app/src/bcsc-theme/features/services/Services.tsx index 5b386035b..834193e6a 100644 --- a/app/src/bcsc-theme/features/services/Services.tsx +++ b/app/src/bcsc-theme/features/services/Services.tsx @@ -24,9 +24,9 @@ const mockHeaderText = 'Browse websites you can log in to with this app' * @return {*} {JSX.Element} The Services screen component. */ const Services: React.FC = () => { - const { Spacing } = useTheme() const { metadata } = useApi() const [store] = useStore() + const { ColorPalette, Spacing, TextTheme } = useTheme() const navigation = useNavigation() const [logger] = useServices([TOKENS.UTIL_LOGGER]) @@ -38,6 +38,16 @@ const Services: React.FC = () => { paddingHorizontal: Spacing.md, paddingVertical: Spacing.lg, }, + searchInput: { + height: 60, + borderRadius: 24, + color: TextTheme.normal.color, + backgroundColor: ColorPalette.brand.secondaryBackground, + marginHorizontal: Spacing.lg, + marginBottom: Spacing.lg, + fontSize: TextTheme.headerTitle.fontSize, + padding: Spacing.md, + }, }) const { @@ -100,6 +110,7 @@ const Services: React.FC = () => { onChange={(event) => { setSearchText(event.nativeEvent.text) }} + style={styles.searchInput} /> {filteredServices.map((service) => ( Date: Thu, 11 Sep 2025 17:20:42 -0700 Subject: [PATCH 09/34] chore: added localizations for search + sorted services alphabetically Signed-off-by: Mac Deluca --- .../bcsc-theme/features/services/Services.tsx | 33 +++++++++++-------- app/src/localization/en/index.ts | 1 + app/src/localization/fr/index.ts | 1 + app/src/localization/pt-br/index.ts | 1 + 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/app/src/bcsc-theme/features/services/Services.tsx b/app/src/bcsc-theme/features/services/Services.tsx index 834193e6a..8afadf244 100644 --- a/app/src/bcsc-theme/features/services/Services.tsx +++ b/app/src/bcsc-theme/features/services/Services.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import TabScreenWrapper from '@/bcsc-theme/components/TabScreenWrapper' -import { ThemedText, TOKENS, useServices, useStore, useTheme } from '@bifold/core' +import { testIdWithKey, ThemedText, TOKENS, useServices, useStore, useTheme } from '@bifold/core' import { Alert, StyleSheet, TextInput } from 'react-native' import ServiceButton from './components/ServiceButton' import useApi from '@/bcsc-theme/api/hooks/useApi' @@ -12,6 +12,7 @@ import { useDebounce } from '@/hooks/useDebounce' import { useNavigation } from '@react-navigation/native' import { BCSCRootStackParams, BCSCScreens } from '@/bcsc-theme/types/navigators' import { StackNavigationProp } from '@react-navigation/stack' +import { useTranslation } from 'react-i18next' type ServicesNavigationProp = StackNavigationProp @@ -25,13 +26,14 @@ const mockHeaderText = 'Browse websites you can log in to with this app' */ const Services: React.FC = () => { const { metadata } = useApi() + const { t } = useTranslation() const [store] = useStore() const { ColorPalette, Spacing, TextTheme } = useTheme() const navigation = useNavigation() const [logger] = useServices([TOKENS.UTIL_LOGGER]) - const [searchText, setSearchText] = useState('') - const debouncedSearchText = useDebounce(searchText, 300) + const [search, setSearch] = useState('') + const debouncedSearch = useDebounce(search, 300) // delayed by 300ms const styles = StyleSheet.create({ headerText: { @@ -71,19 +73,22 @@ const Services: React.FC = () => { } // Filter services based on the user's card type (ie: card process) - const supportedServices = services.filter((service) => - service.allowed_identification_processes.includes(getCardProcessForCardType(store.bcsc.cardType)) - ) + const supportedServices = services + .filter((service) => + service.allowed_identification_processes.includes(getCardProcessForCardType(store.bcsc.cardType)) + ) + // Sort services alphabetically by client_name + .sort((a, b) => a.client_name.localeCompare(b.client_name)) // Return all supported services when there's no search text - if (debouncedSearchText.trim() === '') { + if (debouncedSearch.trim() === '') { return supportedServices } // Filter supported services based on the search text (case insensitive) - const query = debouncedSearchText.toLowerCase() + const query = debouncedSearch.toLowerCase() return supportedServices.filter((service) => service.client_name.toLowerCase().includes(query)) - }, [services, debouncedSearchText, store.bcsc.cardType]) + }, [services, debouncedSearch, store.bcsc.cardType]) // Alert the user if services fail to load if (!services && isReady) { @@ -105,11 +110,13 @@ const Services: React.FC = () => { {mockHeaderText} { - setSearchText(event.nativeEvent.text) + setSearch(event.nativeEvent.text) }} + accessibilityLabel={t('Services.CatalogueSearch')} + testID={testIdWithKey('search')} style={styles.searchInput} /> {filteredServices.map((service) => ( diff --git a/app/src/localization/en/index.ts b/app/src/localization/en/index.ts index b7c8c6bac..08cb88bde 100644 --- a/app/src/localization/en/index.ts +++ b/app/src/localization/en/index.ts @@ -217,6 +217,7 @@ const translation = { }, "Services": { "CatalogueTitle": "Browse websites you can log in to with this app", + "CatalogueSearch": "search", "ServiceLoginInstructions": "You will need to go to their website first if you want to log in to it. You can't log in to services directly from this app.", "ServiceLoginProof": "You will use this app to prove who you are when you log in.", "ServiceGoto": "Go to", diff --git a/app/src/localization/fr/index.ts b/app/src/localization/fr/index.ts index 457813f44..3351cd97e 100644 --- a/app/src/localization/fr/index.ts +++ b/app/src/localization/fr/index.ts @@ -214,6 +214,7 @@ const translation = { }, "Services": { "CatalogueTitle": "Browse websites you can log in to with this app (FR)", + "CatalogueSearch": "search (FR)", "ServiceLoginInstructions": "You will need to go to their website first if you want to log in to it. You can't log in to services directly from this app. (FR)", "ServiceLoginProof": "You will use this app to prove who you are when you log in. (FR)", "ServiceGoto": "Go to (FR)", diff --git a/app/src/localization/pt-br/index.ts b/app/src/localization/pt-br/index.ts index 8448e09d4..ef3fbd1ee 100644 --- a/app/src/localization/pt-br/index.ts +++ b/app/src/localization/pt-br/index.ts @@ -214,6 +214,7 @@ const translation = { }, "Services": { "CatalogueTitle": "Browse websites you can log in to with this app (PT-BR)", + "CatalogueSearch": "search (PT-BR)", "ServiceLoginInstructions": "You will need to go to their website first if you want to log in to it. You can't log in to services directly from this app. (PT-BR)", "ServiceLoginProof": "You will use this app to prove who you are when you log in. (PT-BR)", "ServiceGoto": "Go to (PT-BR)", From 945351b93fc82947b634c9dddedcb71801ac274e Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Fri, 12 Sep 2025 09:23:52 -0700 Subject: [PATCH 10/34] chore: updated placeholder text colour Signed-off-by: Mac Deluca --- app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx | 1 - app/src/bcsc-theme/features/services/Services.tsx | 1 + app/src/bcsc-theme/fixtures/services.ts | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx index 612237bc3..8d80b899f 100644 --- a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx +++ b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx @@ -1,6 +1,5 @@ import { BCSCRootStackParams, BCSCScreens } from '@/bcsc-theme/types/navigators' import { Link, ThemedText, useTheme } from '@bifold/core' -import { useNavigation } from '@react-navigation/native' import { StackScreenProps } from '@react-navigation/stack' import { useTranslation } from 'react-i18next' import { StyleSheet, View } from 'react-native' diff --git a/app/src/bcsc-theme/features/services/Services.tsx b/app/src/bcsc-theme/features/services/Services.tsx index 8afadf244..ad4699bf8 100644 --- a/app/src/bcsc-theme/features/services/Services.tsx +++ b/app/src/bcsc-theme/features/services/Services.tsx @@ -111,6 +111,7 @@ const Services: React.FC = () => { { setSearch(event.nativeEvent.text) diff --git a/app/src/bcsc-theme/fixtures/services.ts b/app/src/bcsc-theme/fixtures/services.ts index 99d424a96..5719a527f 100644 --- a/app/src/bcsc-theme/fixtures/services.ts +++ b/app/src/bcsc-theme/fixtures/services.ts @@ -1,3 +1,4 @@ +// @deprecated - remove after Services screens fully implemented export interface ServiceData { id: string title: string @@ -5,6 +6,7 @@ export interface ServiceData { onPress: () => void } +// @deprecated - remove after Services screens fully implemented export const mockServices: ServiceData[] = [ { id: '1', From a57195499ff1480e335b52dd09ab35a334ff97c1 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Fri, 12 Sep 2025 10:11:38 -0700 Subject: [PATCH 11/34] feat: search now sticky Signed-off-by: Mac Deluca --- .../services/ServiceDetailsScreen.tsx | 34 +++--- .../bcsc-theme/features/services/Services.tsx | 100 ++++++++++-------- 2 files changed, 79 insertions(+), 55 deletions(-) diff --git a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx index 8d80b899f..0332de8c5 100644 --- a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx +++ b/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx @@ -2,7 +2,7 @@ import { BCSCRootStackParams, BCSCScreens } from '@/bcsc-theme/types/navigators' import { Link, ThemedText, useTheme } from '@bifold/core' import { StackScreenProps } from '@react-navigation/stack' import { useTranslation } from 'react-i18next' -import { StyleSheet, View } from 'react-native' +import { Linking, StyleSheet, View } from 'react-native' type ServiceDetailsScreenProps = StackScreenProps @@ -14,13 +14,20 @@ type ServiceDetailsScreenProps = StackScreenProps = (props: ServiceDetailsScreenProps) => { const { service } = props.route.params const { t } = useTranslation() - const { Spacing } = useTheme() + const { Spacing, ColorPalette, TextTheme } = useTheme() const styles = StyleSheet.create({ screenContainer: { flex: 1, padding: Spacing.lg, - gap: Spacing.lg, + gap: Spacing.md * 2, + }, + divider: { + height: 1, + backgroundColor: ColorPalette.grayscale.mediumGrey, + }, + link: { + color: ColorPalette.brand.primary, }, }) @@ -31,16 +38,19 @@ export const ServiceDetailsScreen: React.FC = (props: {t('Services.ServiceLoginProof')} { - props.navigation.navigate(BCSCScreens.WebView, { - url: service.client_uri, - title: service.client_name, - }) - }} + onPress={() => Linking.openURL(service.client_uri)} + style={styles.link} > - {t('Services.ServicePreferComputer')} - {t('Services.ServicePreferComputerHelp')} - {}}> + + + {t('Services.ServicePreferComputer')} + {t('Services.ServicePreferComputerHelp')} + Linking.openURL(service.client_uri)} + style={styles.link} + > + ) } diff --git a/app/src/bcsc-theme/features/services/Services.tsx b/app/src/bcsc-theme/features/services/Services.tsx index ad4699bf8..38cea55b0 100644 --- a/app/src/bcsc-theme/features/services/Services.tsx +++ b/app/src/bcsc-theme/features/services/Services.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useMemo, useState } from 'react' -import TabScreenWrapper from '@/bcsc-theme/components/TabScreenWrapper' import { testIdWithKey, ThemedText, TOKENS, useServices, useStore, useTheme } from '@bifold/core' -import { Alert, StyleSheet, TextInput } from 'react-native' +import { Alert, ScrollView, StyleSheet, TextInput, View } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' import ServiceButton from './components/ServiceButton' import useApi from '@/bcsc-theme/api/hooks/useApi' import useDataLoader from '@/bcsc-theme/hooks/useDataLoader' @@ -14,10 +14,9 @@ import { BCSCRootStackParams, BCSCScreens } from '@/bcsc-theme/types/navigators' import { StackNavigationProp } from '@react-navigation/stack' import { useTranslation } from 'react-i18next' -type ServicesNavigationProp = StackNavigationProp +const SEARCH_DEBOUNCE_DELAY_MS = 300 -// to be replaced with API response or translation entries, whichever ends up being the case -const mockHeaderText = 'Browse websites you can log in to with this app' +type ServicesNavigationProp = StackNavigationProp /** * Services screen component that displays a list of services accessible with the user's BCSC card. @@ -33,13 +32,16 @@ const Services: React.FC = () => { const [logger] = useServices([TOKENS.UTIL_LOGGER]) const [search, setSearch] = useState('') - const debouncedSearch = useDebounce(search, 300) // delayed by 300ms + const debouncedSearch = useDebounce(search, SEARCH_DEBOUNCE_DELAY_MS) const styles = StyleSheet.create({ headerText: { paddingHorizontal: Spacing.md, paddingVertical: Spacing.lg, }, + searchContainer: { + backgroundColor: ColorPalette.brand.primaryBackground, + }, searchInput: { height: 60, borderRadius: 24, @@ -66,20 +68,25 @@ const Services: React.FC = () => { load() }, [load]) - // Filter services based on the user's card type and search text - const filteredServices = useMemo(() => { + // Services that are compatible with the user's card type + const supportedServices = useMemo(() => { if (!services) { return [] } // Filter services based on the user's card type (ie: card process) - const supportedServices = services - .filter((service) => - service.allowed_identification_processes.includes(getCardProcessForCardType(store.bcsc.cardType)) - ) - // Sort services alphabetically by client_name - .sort((a, b) => a.client_name.localeCompare(b.client_name)) + return ( + services + .filter((service) => + service.allowed_identification_processes.includes(getCardProcessForCardType(store.bcsc.cardType)) + ) + // Sort services alphabetically by client_name + .sort((a, b) => a.client_name.localeCompare(b.client_name)) + ) + }, [services, store.bcsc.cardType]) + // Filter services based on the search text + const filteredServices = useMemo(() => { // Return all supported services when there's no search text if (debouncedSearch.trim() === '') { return supportedServices @@ -88,7 +95,7 @@ const Services: React.FC = () => { // Filter supported services based on the search text (case insensitive) const query = debouncedSearch.toLowerCase() return supportedServices.filter((service) => service.client_name.toLowerCase().includes(query)) - }, [services, debouncedSearch, store.bcsc.cardType]) + }, [supportedServices, debouncedSearch]) // Alert the user if services fail to load if (!services && isReady) { @@ -105,34 +112,41 @@ const Services: React.FC = () => { // TODO (MD): implement a loading UI return ( - - - {mockHeaderText} - - { - setSearch(event.nativeEvent.text) - }} - accessibilityLabel={t('Services.CatalogueSearch')} - testID={testIdWithKey('search')} - style={styles.searchInput} - /> - {filteredServices.map((service) => ( - { - navigation.navigate(BCSCScreens.ServiceDetailsScreen, { - service, - }) - }} - /> - ))} - + + + + {t('Services.CatalogueTitle')} + + + { + setSearch(event.nativeEvent.text) + }} + accessibilityLabel={t('Services.CatalogueSearch')} + testID={testIdWithKey('search')} + style={styles.searchInput} + /> + + {filteredServices.map((service) => ( + { + navigation.navigate(BCSCScreens.ServiceDetailsScreen, { + service, + }) + }} + /> + ))} + + ) } From e8b23777f6dbea1c2796c5c2d6392549924bf422 Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Fri, 12 Sep 2025 10:24:10 -0700 Subject: [PATCH 12/34] chore: renamed ServiceDetailsScreen -> ServiceLoginScreen Signed-off-by: Mac Deluca --- ...tailsScreen.tsx => ServiceLoginScreen.tsx} | 24 +++++++++---------- .../bcsc-theme/features/services/Services.tsx | 4 ++-- app/src/bcsc-theme/navigators/MainStack.tsx | 6 ++--- app/src/bcsc-theme/types/navigators.ts | 4 ++-- 4 files changed, 19 insertions(+), 19 deletions(-) rename app/src/bcsc-theme/features/services/{ServiceDetailsScreen.tsx => ServiceLoginScreen.tsx} (68%) diff --git a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx b/app/src/bcsc-theme/features/services/ServiceLoginScreen.tsx similarity index 68% rename from app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx rename to app/src/bcsc-theme/features/services/ServiceLoginScreen.tsx index 0332de8c5..bbb2fffd8 100644 --- a/app/src/bcsc-theme/features/services/ServiceDetailsScreen.tsx +++ b/app/src/bcsc-theme/features/services/ServiceLoginScreen.tsx @@ -4,14 +4,14 @@ import { StackScreenProps } from '@react-navigation/stack' import { useTranslation } from 'react-i18next' import { Linking, StyleSheet, View } from 'react-native' -type ServiceDetailsScreenProps = StackScreenProps +type ServiceLoginScreenProps = StackScreenProps /** * Renders the service details screen component, which displays information about a specific service. * * @returns {*} {JSX.Element} The service screen component or null if not implemented. */ -export const ServiceDetailsScreen: React.FC = (props: ServiceDetailsScreenProps) => { +export const ServiceLoginScreen: React.FC = (props) => { const { service } = props.route.params const { t } = useTranslation() const { Spacing, ColorPalette, TextTheme } = useTheme() @@ -41,16 +41,16 @@ export const ServiceDetailsScreen: React.FC = (props: onPress={() => Linking.openURL(service.client_uri)} style={styles.link} > - - - {t('Services.ServicePreferComputer')} - {t('Services.ServicePreferComputerHelp')} - Linking.openURL(service.client_uri)} - style={styles.link} - > - + {/* */} + {/* */} + {/* {t('Services.ServicePreferComputer')} */} + {/* {t('Services.ServicePreferComputerHelp')} */} + {/* Linking.openURL(service.client_uri)} */} + {/* style={styles.link} */} + {/* > */} + {/* */} ) } diff --git a/app/src/bcsc-theme/features/services/Services.tsx b/app/src/bcsc-theme/features/services/Services.tsx index 38cea55b0..c83dadf5a 100644 --- a/app/src/bcsc-theme/features/services/Services.tsx +++ b/app/src/bcsc-theme/features/services/Services.tsx @@ -16,7 +16,7 @@ import { useTranslation } from 'react-i18next' const SEARCH_DEBOUNCE_DELAY_MS = 300 -type ServicesNavigationProp = StackNavigationProp +type ServicesNavigationProp = StackNavigationProp /** * Services screen component that displays a list of services accessible with the user's BCSC card. @@ -139,7 +139,7 @@ const Services: React.FC = () => { title={service.client_name} description={service.client_description} onPress={() => { - navigation.navigate(BCSCScreens.ServiceDetailsScreen, { + navigation.navigate(BCSCScreens.ServiceLoginScreen, { service, }) }} diff --git a/app/src/bcsc-theme/navigators/MainStack.tsx b/app/src/bcsc-theme/navigators/MainStack.tsx index cff694c67..863315919 100644 --- a/app/src/bcsc-theme/navigators/MainStack.tsx +++ b/app/src/bcsc-theme/navigators/MainStack.tsx @@ -11,7 +11,7 @@ import BCSCTabStack from './TabStack' import createHelpHeaderButton from '../components/HelpHeaderButton' import { HelpCentreUrl } from '@/constants' import { createWebviewHeaderBackButton } from '../components/WebViewBackButton' -import { ServiceDetailsScreen } from '../features/services/ServiceDetailsScreen' +import { ServiceLoginScreen } from '../features/services/ServiceLoginScreen' const MainStack: React.FC = () => { const { currentStep } = useTour() @@ -68,8 +68,8 @@ const MainStack: React.FC = () => { })} /> ({ headerShown: true, })} diff --git a/app/src/bcsc-theme/types/navigators.ts b/app/src/bcsc-theme/types/navigators.ts index 7424a7f6e..3ba546320 100644 --- a/app/src/bcsc-theme/types/navigators.ts +++ b/app/src/bcsc-theme/types/navigators.ts @@ -45,7 +45,7 @@ export enum BCSCScreens { EvidenceIDCollection = 'BCSCEvidenceIDCollection', ResidentialAddressScreen = 'BCSCResidentialAddressScreen', RemoveAccountConfirmation = 'RemoveAccountConfirmationScreen', - ServiceDetailsScreen = 'ServiceDetailsScreen', + ServiceLoginScreen = 'ServiceLoginScreen', } export type BCSCTabStackParams = { @@ -61,7 +61,7 @@ export type BCSCRootStackParams = { [BCSCScreens.ManualPairingCode]: undefined [BCSCScreens.PairingConfirmation]: { serviceName: string; serviceId: string } [BCSCScreens.RemoveAccountConfirmation]: undefined - [BCSCScreens.ServiceDetailsScreen]: { service: ClientMetadata } + [BCSCScreens.ServiceLoginScreen]: { service: ClientMetadata } } export type BCSCVerifyIdentityStackParams = { From f49bd3393c754e668f90e1129630e43cad33b83a Mon Sep 17 00:00:00 2001 From: Mac Deluca Date: Fri, 12 Sep 2025 13:44:50 -0700 Subject: [PATCH 13/34] feat: added new localizations Signed-off-by: Mac Deluca --- .../bcsc-theme/api/hooks/useMetadataApi.tsx | 20 +++- .../features/services/ServiceLoginScreen.tsx | 100 +++++++++++++----- .../bcsc-theme/features/services/Services.tsx | 3 +- app/src/localization/en/index.ts | 8 +- app/src/localization/fr/index.ts | 8 +- app/src/localization/pt-br/index.ts | 8 +- 6 files changed, 99 insertions(+), 48 deletions(-) diff --git a/app/src/bcsc-theme/api/hooks/useMetadataApi.tsx b/app/src/bcsc-theme/api/hooks/useMetadataApi.tsx index 7ea1cf370..0f9d13bba 100644 --- a/app/src/bcsc-theme/api/hooks/useMetadataApi.tsx +++ b/app/src/bcsc-theme/api/hooks/useMetadataApi.tsx @@ -7,23 +7,33 @@ export type MetadataResponseData = { clients: Clients } +/** + * Client metadata as returned by the IAS Client Metadata endpoint. + * + * @see https://citz-cdt.atlassian.net/wiki/spaces/BMS/pages/301574688/5.1+System+Interfaces#IAS-Client-Metadata-endpoint + */ export interface ClientMetadata { client_ref_id: string client_name: string - policy_uri: string client_uri: string - initiate_login_uri: string application_type: string - client_description: string claims_description: string - service_listing_sort_order: number suppress_confirmation_info: boolean suppress_bookmark_prompt: boolean allowed_identification_processes: string[] + initiate_login_uri?: string + client_description?: string + policy_uri?: string + service_listing_sort_order?: number } const useMetadataApi = () => { - const getClientMetadata = useCallback(async (): Promise => { + /** + * Fetches the client metadata from the IAS Client Metadata endpoint. + * + * @return {*} {Promise} A promise that resolves to an array of client metadata objects. + */ + const getClientMetadata = useCallback(async (): Promise => { const { data: { clients }, } = await apiClient.get(apiClient.endpoints.clientMetadata) diff --git a/app/src/bcsc-theme/features/services/ServiceLoginScreen.tsx b/app/src/bcsc-theme/features/services/ServiceLoginScreen.tsx index bbb2fffd8..3e8992992 100644 --- a/app/src/bcsc-theme/features/services/ServiceLoginScreen.tsx +++ b/app/src/bcsc-theme/features/services/ServiceLoginScreen.tsx @@ -1,8 +1,9 @@ import { BCSCRootStackParams, BCSCScreens } from '@/bcsc-theme/types/navigators' -import { Link, ThemedText, useTheme } from '@bifold/core' +import { Button, ButtonType, Link, testIdWithKey, ThemedText, TOKENS, useServices, useTheme } from '@bifold/core' import { StackScreenProps } from '@react-navigation/stack' import { useTranslation } from 'react-i18next' import { Linking, StyleSheet, View } from 'react-native' +import { SafeAreaView } from 'react-native-safe-area-context' type ServiceLoginScreenProps = StackScreenProps @@ -14,43 +15,88 @@ type ServiceLoginScreenProps = StackScreenProps = (props) => { const { service } = props.route.params const { t } = useTranslation() - const { Spacing, ColorPalette, TextTheme } = useTheme() + const { Spacing, ColorPalette } = useTheme() + const [logger] = useServices([TOKENS.UTIL_LOGGER]) const styles = StyleSheet.create({ screenContainer: { flex: 1, - padding: Spacing.lg, - gap: Spacing.md * 2, + paddingHorizontal: Spacing.lg, + gap: Spacing.xxl, }, - divider: { - height: 1, - backgroundColor: ColorPalette.grayscale.mediumGrey, + buttonContainer: {}, + infoContainer: { + borderRadius: Spacing.sm, + borderColor: ColorPalette.brand.tertiary, + borderWidth: 1, + backgroundColor: ColorPalette.brand.secondaryBackground, + padding: Spacing.md, }, link: { color: ColorPalette.brand.primary, }, }) + const handleLogin = () => {} + + logger.info('Service', { service }) + + // render an alternative screen if the service does not support OIDC login + if (!service.initiate_login_uri) { + return ( + + {service.client_name} + {t('Services.NoLoginInstructions')} + {t('Services.NoLoginProof')} + Linking.openURL(service.client_uri)} + testID={testIdWithKey('ServiceNoLoginLink')} + style={styles.link} + > + + ) + } + return ( - - {service.client_name} - {t('Services.ServiceLoginInstructions')} - {t('Services.ServiceLoginProof')} - Linking.openURL(service.client_uri)} - style={styles.link} - > - {/* */} - {/* */} - {/* {t('Services.ServicePreferComputer')} */} - {/* {t('Services.ServicePreferComputerHelp')} */} - {/* Linking.openURL(service.client_uri)} */} - {/* style={styles.link} */} - {/* > */} - {/* */} - + + + {`${t('Services.WantToLogIn')}\n`} + {service.client_name}? + + + {t('Services.RequestedInformation')} + + {t('Services.FromAccount')} + {service.claims_description} + + + + {t('Services.PrivacyNotice')} + + + {t('Services.NotYou')} + {t('Services.ReportSuspicious')} + + + + +