├── src ├── navigation │ ├── types.ts │ ├── LoginScreen.tsx │ ├── CustomerQuotaStack │ │ ├── CustomerQuotaScreen.tsx │ │ ├── CustomerAppealScreen.tsx │ │ ├── DailyStatisticsScreen.tsx │ │ └── CollectCustomerDetailsScreen.tsx │ ├── MerchantPayoutStack │ │ ├── PayoutFeedbackScreen.tsx │ │ └── MerchantPayoutScreen.tsx │ └── index.tsx ├── flags.ts ├── services │ ├── campaignConfig │ │ └── index.ts │ └── govwallet │ │ └── balance │ │ └── index.tsx ├── components │ ├── Login │ │ ├── types.ts │ │ ├── LoginScanCard.test.tsx │ │ ├── LoginScanCard.tsx │ │ └── utils.ts │ ├── DailyStatistics │ │ ├── types.ts │ │ └── TransactionHistoryCard.test.tsx │ ├── CustomerQuota │ │ ├── types.ts │ │ ├── ItemsSelection │ │ │ ├── IdentifierLayout │ │ │ │ ├── sharedStyles.tsx │ │ │ │ ├── IdentifierScanButton.tsx │ │ │ │ ├── IdentifierTextInput.tsx │ │ │ │ ├── IdentifierSelectionInput.tsx │ │ │ │ └── IdentifierScanModal.tsx │ │ │ ├── ItemMaxUnitLabel.tsx │ │ │ ├── AddonsItems.tsx │ │ │ ├── sharedStyles.tsx │ │ │ ├── ItemIdentifiersCard.tsx │ │ │ ├── ItemContent.tsx │ │ │ ├── ItemNoQuota.tsx │ │ │ └── ItemStepper.tsx │ │ ├── CheckoutSuccess │ │ │ ├── sharedStyles.tsx │ │ │ ├── RedeemedItem.tsx │ │ │ └── PurchasedItem.tsx │ │ ├── CustomerQuotaProxy.tsx │ │ ├── sharedStyles.tsx │ │ ├── NoQuota │ │ │ ├── styles.ts │ │ │ └── AppealButton.tsx │ │ └── ShowFullListToggle.tsx │ ├── FeatureToggler │ │ └── FeatureToggler.tsx │ ├── MerchantPayout │ │ ├── VoucherStatusModal │ │ │ ├── sharedStyles.tsx │ │ │ └── InvalidCard.tsx │ │ └── ValidVoucherCount.tsx │ ├── Layout │ │ ├── Buttons │ │ │ ├── index.tsx │ │ │ ├── Button.tsx │ │ │ ├── DarkButton.test.tsx │ │ │ ├── BaseButton.test.tsx │ │ │ ├── TransparentButton.tsx │ │ │ ├── DangerButton.tsx │ │ │ ├── HelpButton.tsx │ │ │ ├── BaseButton.tsx │ │ │ ├── SecondaryButton.tsx │ │ │ └── DarkButton.tsx │ │ ├── Card.tsx │ │ ├── AppText.tsx │ │ ├── AppName.tsx │ │ ├── KeyboardAvoidingScrollView.tsx │ │ ├── TopBackground.tsx │ │ ├── AppHeader.tsx │ │ ├── ModalWithClose.tsx │ │ ├── Banner.tsx │ │ └── InputWithLabel.tsx │ ├── Loading │ │ └── index.tsx │ ├── CustomerDetails │ │ ├── sharedStyles.ts │ │ ├── InputPassportSection.test.tsx │ │ ├── InputSelection.tsx │ │ └── InputIdSection.test.tsx │ ├── CustomerAppeal │ │ └── ReasonSelection │ │ │ ├── ReasonSelectionHeader.tsx │ │ │ ├── ReasonSelectionHeader.test.tsx │ │ │ ├── ReasonItem.test.tsx │ │ │ ├── ReasonItem.tsx │ │ │ └── ReasonSelectionCard.test.tsx │ ├── FontLoader │ │ └── index.tsx │ ├── CampaignInitialisation │ │ ├── UpdateByRestartingAlert.tsx │ │ ├── sharedStyles.tsx │ │ ├── UpdateFromAppStoreAlert.tsx │ │ └── utils.ts │ ├── ErrorBoundary │ │ └── ErrorBoundary.tsx │ ├── VoucherScanner │ │ └── ManualAddVoucherModal.tsx │ ├── Credits │ │ └── index.tsx │ └── IdScanner │ │ └── IdScannerLabel.tsx ├── utils │ ├── hash.ts │ ├── validateMerchantCode.tsx │ ├── validateNric.tsx │ ├── validateEmailAddress.tsx │ ├── getIdentifierInputDisplay.ts │ ├── validateInputWithRegex.ts │ ├── currencyFormatter.ts │ ├── validatePassport.ts │ ├── passportScanning.ts │ ├── dateTimeFormatter.ts │ ├── utilsIdentifierInput.ts │ ├── promiseAllSettled.ts │ ├── validateEmailAddress.test.tsx │ ├── currencyFormatter.test.ts │ ├── validatePassport.test.ts │ ├── validateIdentification.ts │ ├── errorTracking.ts │ ├── phoneNumberFormatter.ts │ ├── validateIdentification.test.ts │ ├── dateTimeFormatter.test.ts │ ├── validateVoucherCode.tsx │ ├── validateMerchantCode.test.tsx │ ├── validateVoucherCode.test.tsx │ ├── validateInputWithRegex.test.tsx │ ├── paymentQrValidation.ts │ ├── promiseAllSettled.test.ts │ ├── getIdentifierInputDisplay.test.ts │ └── phoneNumberFormatter.test.ts ├── common │ ├── i18n │ │ ├── i18nMock.ts │ │ └── i18nSetup.ts │ └── styles │ │ ├── borders.tsx │ │ ├── index.tsx │ │ ├── sizes.tsx │ │ ├── shadows.tsx │ │ └── typography.tsx ├── modules.d.ts ├── hooks │ ├── usePrevious.tsx │ ├── useIsMounted.tsx │ └── useAppState.tsx ├── context │ ├── composeProviders.tsx │ ├── auth.tsx │ ├── help.tsx │ ├── campaignConfig.tsx │ ├── products.tsx │ ├── importantMessage.tsx │ ├── theme.tsx │ ├── identification.tsx │ ├── drawer.tsx │ └── config.tsx ├── test │ └── helpers │ │ ├── navigation.tsx │ │ └── providers.tsx └── config │ └── index.tsx ├── storybook ├── rn-addons.js ├── stories │ ├── Logout │ │ ├── index.tsx │ │ └── LogoutScreen.tsx │ ├── Modals │ │ ├── index.tsx │ │ └── Help.tsx │ ├── CustomerDetails │ │ ├── index.tsx │ │ └── CustomerDetailsScreen.tsx │ ├── CustomerAppeal │ │ ├── index.tsx │ │ ├── ReasonSelectionCard.tsx │ │ └── CustomerAppealScreen.tsx │ ├── DailyStatistics │ │ └── index.tsx │ ├── Login │ │ ├── index.tsx │ │ ├── LoginContainer.tsx │ │ ├── MobileNumberCard.tsx │ │ ├── OTPCard.tsx │ │ └── LoginScanCard.tsx │ ├── ErrorBoundary │ │ └── ErrorBoundary.tsx │ ├── index.tsx │ ├── Layout │ │ ├── index.tsx │ │ ├── TopBackground.tsx │ │ ├── Card.tsx │ │ ├── InputWithLabel.tsx │ │ ├── Checkbox.tsx │ │ ├── AppText.tsx │ │ ├── PhoneNumberInput.tsx │ │ ├── Banner.tsx │ │ ├── LightBox.tsx │ │ ├── ModalWithClose.tsx │ │ ├── AppHeader.tsx │ │ ├── AppName.tsx │ │ ├── Stepper.tsx │ │ └── Dropdown.tsx │ ├── decorators │ │ └── index.tsx │ ├── mocks │ │ ├── provider.tsx │ │ └── navigation.tsx │ ├── CampaignLocationScreen │ │ └── index.tsx │ └── CustomerQuota │ │ └── CheckoutUnsuccessful.tsx ├── addons.js ├── index.d.ts ├── config.js └── index.js ├── .gitattributes ├── .vscode └── settings.json ├── assets ├── icon.png ├── blockuser.png ├── splashscreen.png ├── splashscreen-tablet.png ├── fonts │ ├── IBMPlexSans-Bold.ttf │ ├── IBMPlexSans-Italic.ttf │ └── IBMPlexSans-Regular.ttf └── icons │ └── alert.svg ├── .github ├── scripts │ ├── inject-token.sh │ ├── create-profile.sh │ └── set-versions.sh ├── dependabot.yml ├── workflows │ └── scheduled_stale.yaml └── pull_request_template.md ├── .npmrc ├── index.js ├── __mocks__ ├── svg-mock.ts └── expo-camera.tsx ├── .expo-shared └── assets.json ├── .env.example ├── babel.config.js ├── .gitignore ├── sonar-project.properties ├── App.tsx ├── jest.setup.ts ├── eas.json ├── tsconfig.json ├── metro.config.js ├── jest.config.js └── .eslintrc.js /src/navigation/types.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /storybook/rn-addons.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf -------------------------------------------------------------------------------- /storybook/stories/Logout/index.tsx: -------------------------------------------------------------------------------- 1 | import "./LogoutScreen"; 2 | -------------------------------------------------------------------------------- /src/flags.ts: -------------------------------------------------------------------------------- 1 | export const Flags = { 2 | HELP_MODAL: true, 3 | }; 4 | -------------------------------------------------------------------------------- /src/services/campaignConfig/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./campaignConfig"; 2 | -------------------------------------------------------------------------------- /storybook/stories/Modals/index.tsx: -------------------------------------------------------------------------------- 1 | import "./Alert"; 2 | import "./Help"; 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /src/components/Login/types.ts: -------------------------------------------------------------------------------- 1 | export type LoginStage = "SCAN" | "MOBILE_NUMBER" | "OTP"; 2 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rationally-app/mobile-application/HEAD/assets/icon.png -------------------------------------------------------------------------------- /assets/blockuser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rationally-app/mobile-application/HEAD/assets/blockuser.png -------------------------------------------------------------------------------- /storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register"; 2 | import "@storybook/addon-links/register"; 3 | -------------------------------------------------------------------------------- /storybook/stories/CustomerDetails/index.tsx: -------------------------------------------------------------------------------- 1 | import "./CustomerDetailsScreen"; 2 | import "./InputsElements"; 3 | -------------------------------------------------------------------------------- /assets/splashscreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rationally-app/mobile-application/HEAD/assets/splashscreen.png -------------------------------------------------------------------------------- /storybook/stories/CustomerAppeal/index.tsx: -------------------------------------------------------------------------------- 1 | import "./CustomerAppealScreen"; 2 | import "./ReasonSelectionCard"; 3 | -------------------------------------------------------------------------------- /storybook/stories/DailyStatistics/index.tsx: -------------------------------------------------------------------------------- 1 | import "./DailyStatisticsScreen"; 2 | import "./StatisticsHeader"; 3 | -------------------------------------------------------------------------------- /.github/scripts/inject-token.sh: -------------------------------------------------------------------------------- 1 | echo '//npm.pkg.github.com/:_authToken=${NPM_TOKEN}' | cat - .npmrc > temp && mv temp .npmrc -------------------------------------------------------------------------------- /src/components/DailyStatistics/types.ts: -------------------------------------------------------------------------------- 1 | export type ItemQuantity = { 2 | category: string; 3 | quantity: number; 4 | }; 5 | -------------------------------------------------------------------------------- /storybook/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Component } from "react"; 2 | declare function Storybook(): Component; 3 | export = Storybook; 4 | -------------------------------------------------------------------------------- /assets/splashscreen-tablet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rationally-app/mobile-application/HEAD/assets/splashscreen-tablet.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @rationally-app:registry=https://npm.pkg.github.com 2 | //npm.pkg.github.com/:_authToken=${NPM_TOKEN} 3 | legacy-peer-deps=true 4 | -------------------------------------------------------------------------------- /assets/fonts/IBMPlexSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rationally-app/mobile-application/HEAD/assets/fonts/IBMPlexSans-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/IBMPlexSans-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rationally-app/mobile-application/HEAD/assets/fonts/IBMPlexSans-Italic.ttf -------------------------------------------------------------------------------- /assets/fonts/IBMPlexSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rationally-app/mobile-application/HEAD/assets/fonts/IBMPlexSans-Regular.ttf -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import "expo-dev-client"; 2 | import { registerRootComponent } from "expo"; 3 | import App from "./App"; 4 | 5 | registerRootComponent(App); -------------------------------------------------------------------------------- /storybook/stories/Login/index.tsx: -------------------------------------------------------------------------------- 1 | import "./LoginContainer"; 2 | import "./LoginScanCard"; 3 | import "./MobileNumberCard"; 4 | import "./OTPCard"; 5 | -------------------------------------------------------------------------------- /src/navigation/LoginScreen.tsx: -------------------------------------------------------------------------------- 1 | import { InitialisationContainer } from "../components/Login/LoginContainer"; 2 | 3 | export default InitialisationContainer; 4 | -------------------------------------------------------------------------------- /__mocks__/svg-mock.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/kristerkari/react-native-svg-transformer#usage-with-jest 2 | export default "SvgMock"; 3 | export const ReactComponent = "SvgMock"; 4 | -------------------------------------------------------------------------------- /storybook/config.js: -------------------------------------------------------------------------------- 1 | import { addDecorator } from "@storybook/react-native"; 2 | import { SafeAreaDecorator } from "./stories/decorators"; 3 | 4 | addDecorator(SafeAreaDecorator); 5 | -------------------------------------------------------------------------------- /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, 3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true 4 | } -------------------------------------------------------------------------------- /src/navigation/CustomerQuotaStack/CustomerQuotaScreen.tsx: -------------------------------------------------------------------------------- 1 | import { CustomerQuotaScreen } from "../../components/CustomerQuota/CustomerQuotaScreen"; 2 | 3 | export default CustomerQuotaScreen; 4 | -------------------------------------------------------------------------------- /src/navigation/CustomerQuotaStack/CustomerAppealScreen.tsx: -------------------------------------------------------------------------------- 1 | import { CustomerAppealScreen } from "../../components/CustomerAppeal/CustomerAppealScreen"; 2 | 3 | export default CustomerAppealScreen; 4 | -------------------------------------------------------------------------------- /src/navigation/MerchantPayoutStack/PayoutFeedbackScreen.tsx: -------------------------------------------------------------------------------- 1 | import { PayoutFeedbackScreen } from "../../components/PayoutFeedback/PayoutFeedbackScreen"; 2 | 3 | export default PayoutFeedbackScreen; 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | SENTRY_ORG= 2 | SENTRY_PROJECT= 3 | SENTRY_AUTH_TOKEN= 4 | SENTRY_DSN= 5 | DOMAIN_FORMAT= 6 | APP_BUILD_VERSION=1 7 | APP_BINARY_VERSION=dev 8 | PROJECT_ID= 9 | NPM_TOKEN= 10 | OWNER= -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | api.cache(true); 3 | return { 4 | presets: ["babel-preset-expo"], 5 | plugins: ["inline-dotenv", "react-native-reanimated/plugin"], 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /src/navigation/MerchantPayoutStack/MerchantPayoutScreen.tsx: -------------------------------------------------------------------------------- 1 | import { MerchantPayoutScreenContainer } from "../../components/MerchantPayout/MerchantPayoutScreen"; 2 | 3 | export default MerchantPayoutScreenContainer; 4 | -------------------------------------------------------------------------------- /src/navigation/CustomerQuotaStack/DailyStatisticsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { DailyStatisticsScreenContainer } from "../../components/DailyStatistics/DailyStatisticsScreen"; 2 | 3 | export default DailyStatisticsScreenContainer; 4 | -------------------------------------------------------------------------------- /src/navigation/CustomerQuotaStack/CollectCustomerDetailsScreen.tsx: -------------------------------------------------------------------------------- 1 | import { CollectCustomerDetailsScreen } from "../../components/CustomerDetails/CollectCustomerDetailsScreen"; 2 | 3 | export default CollectCustomerDetailsScreen; 4 | -------------------------------------------------------------------------------- /src/utils/hash.ts: -------------------------------------------------------------------------------- 1 | import { digestStringAsync, CryptoDigestAlgorithm } from "expo-crypto"; 2 | 3 | export const hashString = async (str: string): Promise => 4 | await digestStringAsync(CryptoDigestAlgorithm.SHA256, str); 5 | -------------------------------------------------------------------------------- /src/common/i18n/i18nMock.ts: -------------------------------------------------------------------------------- 1 | import { zh } from "./translations/zh"; 2 | import { en } from "./translations/en"; 3 | import i18n from "i18n-js"; 4 | 5 | i18n.fallbacks = true; 6 | i18n.locale = "en"; 7 | i18n.translations = { 8 | zh, 9 | en, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | .cert 13 | .env 14 | .idea 15 | 16 | # macOS 17 | .DS_Store 18 | coverage/ 19 | .scannerwork/ -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /.github/scripts/create-profile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | stage=$1 4 | 5 | cat <<< "$(jq \ 6 | --arg stage "$stage" \ 7 | '.build[$stage].env.NODE_OPTIONS = "--max_old_space_size=7168"' eas.json)" > eas.json 8 | 9 | cat <<< "$(jq \ 10 | --arg stage "$stage" \ 11 | '.build[$stage].channel = $stage' eas.json)" > eas.json 12 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=Open-Attestation_identity-wallet 2 | sonar.organization=open-attestation 3 | sonar.projectName=identity-wallet 4 | sonar.projectVersion=1.0 5 | sonar.test.inclusions=src/**/*.test.ts,src/**/*.test.tsx 6 | sonar.inclusions=src/** 7 | sonar.typescript.lcov.reportPaths=coverage/lcov.info 8 | -------------------------------------------------------------------------------- /src/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | import { SvgProps } from "react-native-svg"; 3 | const content: React.StatelessComponent; 4 | export default content; 5 | } 6 | 7 | declare module "@react-native-async-storage/async-storage/jest/async-storage-mock" { 8 | export default "*" as any; 9 | } 10 | -------------------------------------------------------------------------------- /src/common/styles/borders.tsx: -------------------------------------------------------------------------------- 1 | export type BorderRadiusLevel = 1 | 2 | 3 | 4 | 5; 2 | 3 | /** 4 | * Returns the border radius for the given level 5 | * 6 | * @param level 1 (2), 2 (4), 3 (8), 4 (16), 5 (24) 7 | */ 8 | export const borderRadius = (level: BorderRadiusLevel): number => 9 | [2, 4, 8, 16, 24][level - 1]; 10 | -------------------------------------------------------------------------------- /storybook/stories/ErrorBoundary/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { ErrorBoundaryContent } from "../../../src/components/ErrorBoundary/ErrorBoundaryContent"; 4 | 5 | storiesOf("ErrorBoundary", module).add("ErrorBoundary", () => ( 6 | 7 | )); 8 | -------------------------------------------------------------------------------- /src/hooks/usePrevious.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect } from "react"; 2 | 3 | export function usePrevious(value: T): T | undefined { 4 | const ref = useRef(); 5 | useEffect(() => { 6 | ref.current = value; 7 | }, [value]); 8 | // Refers to the previous value since useEffect will update 9 | // after this value has already been returned. 10 | return ref.current; 11 | } 12 | -------------------------------------------------------------------------------- /storybook/stories/index.tsx: -------------------------------------------------------------------------------- 1 | import "./CampaignLocationScreen"; 2 | import "./CustomerQuota/CheckoutSuccessCard"; 3 | import "./CustomerQuota/CheckoutUnsuccessful"; 4 | import "./CustomerAppeal"; 5 | import "./CustomerDetails"; 6 | import "./DailyStatistics"; 7 | import "./ErrorBoundary/ErrorBoundary"; 8 | import "./Layout"; 9 | import "./Login"; 10 | import "./Modals"; 11 | import "./Logout"; 12 | -------------------------------------------------------------------------------- /src/utils/validateMerchantCode.tsx: -------------------------------------------------------------------------------- 1 | import { ERROR_MESSAGE } from "../context/alert"; 2 | 3 | const MERCHANT_CODE_REGEX = /^[a-zA-Z0-9]{1,8}$/; 4 | 5 | export const validateMerchantCode = (merchantCode: string): boolean => { 6 | const merchantCodeArr = merchantCode.match(MERCHANT_CODE_REGEX); 7 | if (!merchantCodeArr) throw new Error(ERROR_MESSAGE.INVALID_MERCHANT_CODE); 8 | return true; 9 | }; 10 | -------------------------------------------------------------------------------- /storybook/stories/Layout/index.tsx: -------------------------------------------------------------------------------- 1 | import "./AppHeader"; 2 | import "./AppName"; 3 | import "./AppText"; 4 | import "./Banner"; 5 | import "./Button"; 6 | import "./Card"; 7 | import "./Checkbox"; 8 | import "./Dropdown"; 9 | import "./InputWithLabel"; 10 | import "./LightBox"; 11 | import "./ModalWithClose"; 12 | import "./PhoneNumberInput"; 13 | import "./Stepper"; 14 | import "./TopBackground"; 15 | -------------------------------------------------------------------------------- /.github/workflows/scheduled_stale.yaml: -------------------------------------------------------------------------------- 1 | name: scheduled stale action 2 | 3 | # Controls when the action will run. 4 | on: 5 | schedule: 6 | - cron: '30 1 * * *' # Every day at UTC 1:30 (SGT 9:30) 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | stale: 12 | uses: rationally-app/ops-aws/.github/workflows/scheduled_stale.yml@master 13 | -------------------------------------------------------------------------------- /App.tsx: -------------------------------------------------------------------------------- 1 | import AppNavigation from "./src/navigation"; 2 | import { Sentry } from "./src/utils/errorTracking"; 3 | import Storybook from "./storybook"; 4 | import { IS_STORYBOOK_VIEW, SENTRY_DSN } from "./src/config"; 5 | 6 | Sentry.init({ 7 | dsn: SENTRY_DSN, 8 | // enableInExpoDevelopment: true, 9 | debug: __DEV__, 10 | }); 11 | 12 | export default IS_STORYBOOK_VIEW ? Storybook : AppNavigation; 13 | -------------------------------------------------------------------------------- /src/components/CustomerQuota/types.ts: -------------------------------------------------------------------------------- 1 | import { IdentifierInput } from "../../types"; 2 | 3 | export interface CartState { 4 | [category: string]: boolean | null; 5 | } 6 | 7 | export type ItemQuantities = { 8 | category: string; 9 | quantities: { 10 | [id: string]: number; 11 | }; 12 | identifierInputs?: IdentifierInput[]; 13 | }; 14 | 15 | export type CheckoutQuantitiesByItem = ItemQuantities[]; 16 | -------------------------------------------------------------------------------- /src/components/FeatureToggler/FeatureToggler.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, PropsWithChildren } from "react"; 2 | import { Flags } from "../../flags"; 3 | 4 | export const FeatureToggler: FunctionComponent< 5 | PropsWithChildren<{ 6 | feature: keyof typeof Flags; 7 | }> 8 | > = ({ feature, children }) => { 9 | if (Flags[feature]) { 10 | return <>{children}; 11 | } 12 | return null; 13 | }; 14 | -------------------------------------------------------------------------------- /src/common/styles/index.tsx: -------------------------------------------------------------------------------- 1 | import { color } from "./colors"; 2 | import { shadow } from "./shadows"; 3 | import { size } from "./sizes"; 4 | import { letterSpacing, fontSize, normalize, lineHeight } from "./typography"; 5 | import { borderRadius } from "./borders"; 6 | 7 | export { 8 | color, 9 | shadow, 10 | size, 11 | letterSpacing, 12 | fontSize, 13 | borderRadius, 14 | normalize, 15 | lineHeight, 16 | }; 17 | -------------------------------------------------------------------------------- /src/common/styles/sizes.tsx: -------------------------------------------------------------------------------- 1 | type SizeLevel = 0.5 | 1 | 1.5 | 2 | 2.5 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10; 2 | const base = 8; 3 | 4 | /** 5 | * Returns the size which is a multiple of the base of 8 6 | * 7 | * @param level 8 | * 0.5 (4), 1 (8), 1.5 (12), 2 (16), 2.5 (20), 3 (24), 9 | * 4 (32), 5 (40), 6 (48), 7 (56), 8 (64), 9 (72), 10 (80) 10 | */ 11 | export const size = (level: SizeLevel): number => level * base; 12 | -------------------------------------------------------------------------------- /storybook/stories/Layout/TopBackground.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { TopBackground } from "../../../src/components/Layout/TopBackground"; 4 | import { AppMode } from "../../../src/context/config"; 5 | 6 | storiesOf("Layout/TopBackground", module) 7 | .add("Production", () => ) 8 | .add("Staging", () => ); 9 | -------------------------------------------------------------------------------- /src/hooks/useIsMounted.tsx: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, useCallback } from "react"; 2 | 3 | export const useIsMounted = (): (() => boolean) => { 4 | const mountedRef = useRef(false); 5 | const isMounted = useCallback(() => mountedRef.current, []); 6 | useEffect(() => { 7 | mountedRef.current = true; 8 | return () => { 9 | mountedRef.current = false; 10 | }; 11 | }, []); 12 | return isMounted; 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/MerchantPayout/VoucherStatusModal/sharedStyles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { size, fontSize } from "../../../common/styles"; 3 | 4 | export const sharedStyles = StyleSheet.create({ 5 | statusTitleWrapper: { 6 | marginBottom: size(2), 7 | }, 8 | statusTitle: { 9 | fontSize: fontSize(3), 10 | lineHeight: 1.3 * fontSize(3), 11 | fontFamily: "brand-bold", 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /.github/scripts/set-versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | stage=$1 4 | build_version=$2 5 | binary_version=$3 6 | 7 | cat <<< "$(jq \ 8 | --arg stage "$stage" \ 9 | --arg build_version "$build_version" \ 10 | '.build[$stage].env.APP_BUILD_VERSION = $build_version' eas.json)" > eas.json 11 | 12 | cat <<< "$(jq \ 13 | --arg stage "$stage" \ 14 | --arg binary_version "$binary_version" \ 15 | '.build[$stage].env.APP_BINARY_VERSION = $binary_version' eas.json)" > eas.json 16 | -------------------------------------------------------------------------------- /src/utils/validateNric.tsx: -------------------------------------------------------------------------------- 1 | import { validate } from "@rationally-app/nric-validator"; 2 | import { ERROR_MESSAGE } from "../context/alert"; 3 | 4 | export const validateAndCleanNric = (inputNric: string): string => { 5 | const cleanedInputNric = inputNric.trim().slice(0, 9).toUpperCase(); 6 | const isNricValid = validate(cleanedInputNric); 7 | if (!isNricValid) { 8 | throw new Error(ERROR_MESSAGE.INVALID_ID); 9 | } 10 | return cleanedInputNric; 11 | }; 12 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-native/extend-expect"; 2 | import "isomorphic-fetch"; 3 | import mockAsyncStorage from "@react-native-async-storage/async-storage/jest/async-storage-mock"; 4 | 5 | jest.mock("@react-native-async-storage/async-storage", () => mockAsyncStorage); 6 | jest.mock("react-native/Libraries/Vibration/Vibration", () => ({ 7 | vibrate: () => "Vibration.vibrate", 8 | })); 9 | jest.mock("react-native/Libraries/Animated/NativeAnimatedHelper"); 10 | -------------------------------------------------------------------------------- /src/components/Layout/Buttons/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "./Button"; 2 | import { DangerButton } from "./DangerButton"; 3 | import { DarkButton } from "./DarkButton"; 4 | import { HelpButton } from "./HelpButton"; 5 | import { SecondaryButton } from "./SecondaryButton"; 6 | import { TransparentButton } from "./TransparentButton"; 7 | 8 | export { 9 | Button, 10 | DarkButton, 11 | DangerButton, 12 | HelpButton, 13 | SecondaryButton, 14 | TransparentButton, 15 | }; 16 | -------------------------------------------------------------------------------- /src/utils/validateEmailAddress.tsx: -------------------------------------------------------------------------------- 1 | import { ERROR_MESSAGE } from "../context/alert"; 2 | 3 | const EMAIL_CODE_REGEX = 4 | /^[0-9a-zA-Z]+([0-9a-zA-Z]*[-._+])*[0-9a-zA-Z]+@[0-9a-zA-Z]+([-.][0-9a-zA-Z]+)*([0-9a-zA-Z]*[.])[a-zA-Z]{2,6}$/; 5 | 6 | export const validateEmailAddress = (emailAddress: string): string => { 7 | const result = emailAddress.trim().match(EMAIL_CODE_REGEX); 8 | if (!result) throw new Error(ERROR_MESSAGE.INVALID_EMAIL_ADDRESS); 9 | return emailAddress; 10 | }; 11 | -------------------------------------------------------------------------------- /storybook/stories/Login/LoginContainer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { View } from "react-native"; 4 | import { InitialisationContainer } from "../../../src/components/Login/LoginContainer"; 5 | import { navigation } from "../mocks/navigation"; 6 | 7 | storiesOf("Screen", module).add("LoginScreen", () => ( 8 | 9 | 10 | 11 | )); 12 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": "3.12.0" 4 | }, 5 | "build": { 6 | "preview": { 7 | "developmentClient": true, 8 | "distribution": "internal", 9 | "env": { 10 | "NODE_OPTIONS": "--max_old_space_size=7168" 11 | }, 12 | "channel": "preview" 13 | }, 14 | "staging": { 15 | "distribution": "internal", 16 | "env": { 17 | "NODE_OPTIONS": "--max_old_space_size=7168" 18 | }, 19 | "channel": "staging" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "allowSyntheticDefaultImports": true, 5 | "jsx": "react-native", 6 | "lib": ["dom", "esnext"], 7 | "moduleResolution": "node", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "resolveJsonModule": true, 11 | "noImplicitAny": true, 12 | "useUnknownInCatchVariables": false, 13 | "types": ["node", "jest"], 14 | "target": "esnext" 15 | }, 16 | "includes": ["src"], 17 | "extends": "expo/tsconfig.base" 18 | } 19 | -------------------------------------------------------------------------------- /src/components/CustomerQuota/ItemsSelection/IdentifierLayout/sharedStyles.tsx: -------------------------------------------------------------------------------- 1 | import { StyleSheet } from "react-native"; 2 | import { size } from "../../../../common/styles"; 3 | 4 | export const sharedStyles = StyleSheet.create({ 5 | inputAndButtonWrapper: { 6 | flexDirection: "row", 7 | justifyContent: "center", 8 | alignItems: "flex-end", 9 | width: "100%", 10 | marginTop: size(2), 11 | }, 12 | inputWrapper: { 13 | flex: 2, 14 | }, 15 | buttonWrapper: { 16 | flexShrink: 1, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const { getDefaultConfig } = require("expo/metro-config"); 3 | 4 | const config = getDefaultConfig(__dirname); 5 | 6 | config.transformer = { 7 | ...config.transformer, 8 | babelTransformerPath: require.resolve("react-native-svg-transformer"), 9 | }; 10 | 11 | config.resolver = { 12 | assetExts: config.resolver.assetExts.filter((ext) => ext !== "svg"), 13 | sourceExts: [...config.resolver.sourceExts, "svg"], 14 | }; 15 | 16 | module.exports = config; 17 | -------------------------------------------------------------------------------- /src/utils/getIdentifierInputDisplay.ts: -------------------------------------------------------------------------------- 1 | import { IdentifierInput } from "../types"; 2 | 3 | const processIdentifierInputValue = ( 4 | identifierInput: IdentifierInput 5 | ): string => { 6 | return identifierInput.value; 7 | }; 8 | 9 | export const getIdentifierInputDisplay = ( 10 | identifierInputs: IdentifierInput[] 11 | ): string => { 12 | const filteredInputs = identifierInputs.filter( 13 | (identifierInput) => !!identifierInput.value 14 | ); 15 | 16 | return filteredInputs 17 | .map((input) => processIdentifierInputValue(input)) 18 | .join("\n"); 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/validateInputWithRegex.ts: -------------------------------------------------------------------------------- 1 | export const validate = (id: string, idRegex: string): boolean => { 2 | return id.match(idRegex) !== null; 3 | }; 4 | 5 | export const validateAndCleanRegexInput = ( 6 | inputId: string, 7 | idRegex: string 8 | ): string => { 9 | // set ID to all uppercase to remove case sensitivity and removes trailing and leading white spaces 10 | const id = inputId.trim().toUpperCase(); 11 | 12 | const isValid = validate(id, idRegex); 13 | if (!isValid) 14 | throw new Error("Please check that the ID is in the correct format"); 15 | 16 | return id; 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/currencyFormatter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility function that converts a currency amount represented in cents to 3 | * its string representation in dollars and cents. 4 | * 5 | * @param amountInCents Amount in cents 6 | * @param separator Separator character between dollars and cents 7 | * @returns Amount in dollars and cents, as a string 8 | */ 9 | export const formatCentsAsDollarsAndCents = (amountInCents: number): string => { 10 | const amountInDollarsAndCents = amountInCents / 100; 11 | return amountInDollarsAndCents.toLocaleString("en-SG", { 12 | minimumFractionDigits: 2, 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/context/composeProviders.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FunctionComponent, 3 | ComponentType, 4 | ReactElement, 5 | PropsWithChildren, 6 | ReactNode, 7 | } from "react"; 8 | 9 | interface Providers { 10 | providers: ComponentType<{ children?: ReactNode | undefined }>[]; 11 | } 12 | export const Providers: FunctionComponent> = ({ 13 | providers, 14 | children, 15 | }): ReactElement => ( 16 | <> 17 | {providers.reduceRight( 18 | (composed, Provider) => ( 19 | {composed} 20 | ), 21 | <>{children} 22 | )} 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /src/utils/validatePassport.ts: -------------------------------------------------------------------------------- 1 | import { ERROR_MESSAGE } from "../context/alert"; 2 | 3 | const validate = (inputPassport: string): boolean => { 4 | const passportRegex = "^[A-Za-z]{1,3}-[a-zA-Z0-9]{5,9}$"; 5 | return inputPassport.match(passportRegex) !== null; 6 | }; 7 | 8 | export const validateAndCleanPassport = (inputPassport: string): string => { 9 | const cleanedInputPassport = inputPassport.trim().toUpperCase(); 10 | const isPassportValid = validate(cleanedInputPassport); 11 | if (!isPassportValid) { 12 | throw new Error(ERROR_MESSAGE.INVALID_ID); 13 | } 14 | return cleanedInputPassport; 15 | }; 16 | -------------------------------------------------------------------------------- /src/test/helpers/navigation.tsx: -------------------------------------------------------------------------------- 1 | let params: any = {}; 2 | 3 | export const mockNavigation: any = { 4 | navigate: jest.fn(), 5 | dispatch: jest.fn(), 6 | goBack: jest.fn(), 7 | getParam: (key: string) => params[key], 8 | addListener: jest.fn(), 9 | state: { 10 | routeName: "routeName", 11 | }, 12 | }; 13 | 14 | export const resetNavigation = (): void => { 15 | mockNavigation.navigate.mockReset(); 16 | mockNavigation.dispatch.mockReset(); 17 | mockNavigation.goBack.mockReset(); 18 | params = {}; 19 | }; 20 | 21 | export const setParam = (key: string, value: unknown): void => { 22 | params[key] = value; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Layout/Card.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { View, ViewProps } from "react-native"; 3 | import { size, color, borderRadius, shadow } from "../../common/styles"; 4 | 5 | export const Card: FunctionComponent = ({ children, style }) => ( 6 | 19 | {children} 20 | 21 | ); 22 | -------------------------------------------------------------------------------- /storybook/stories/CustomerDetails/CustomerDetailsScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { View } from "react-native"; 4 | import CollectCustomerDetailsScreen from "../../../src/navigation/CustomerQuotaStack/CollectCustomerDetailsScreen"; 5 | import { mockReactNavigationDecorator, navigation } from "../mocks/navigation"; 6 | 7 | storiesOf("Screen", module) 8 | .addDecorator((Story: any) => mockReactNavigationDecorator(Story)) 9 | .add("CollectCustomerDetails", () => ( 10 | 11 | 12 | 13 | )); 14 | -------------------------------------------------------------------------------- /storybook/stories/Login/MobileNumberCard.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { storiesOf } from "@storybook/react-native"; 3 | import { View } from "react-native"; 4 | import { size } from "../../../src/common/styles"; 5 | import { LoginMobileNumberCard } from "../../../src/components/Login/LoginMobileNumberCard"; 6 | 7 | storiesOf("Login", module).add("MobileNumberCard", () => ( 8 | 9 | Promise.resolve(true)} 11 | setMobileNumber={(num) => alert(num)} 12 | setLoginStage={() => null} 13 | setCountryCode={() => null} 14 | /> 15 | 16 | )); 17 | -------------------------------------------------------------------------------- /assets/icons/alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Layout/Buttons/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { color, fontSize } from "../../../common/styles"; 3 | import { BaseButton } from "./BaseButton"; 4 | import { AppText } from "../AppText"; 5 | 6 | export interface Button { 7 | onPress?: () => void; 8 | text: string; 9 | } 10 | 11 | export const Button: FunctionComponent