diff --git a/app/Gemfile b/app/Gemfile index 7e41c8b9c..5422aa99c 100644 --- a/app/Gemfile +++ b/app/Gemfile @@ -8,11 +8,12 @@ gem "cocoapods", ">= 1.13", "!= 1.15.0", "!= 1.15.1" gem "activesupport", ">= 6.1.7.5", "!= 7.1.0" # Add fastlane for CI/CD -gem "fastlane", "~> 2.230.0" +gem "fastlane", "~> 2.232.0" group :development do gem "dotenv" gem "nokogiri", "~> 1.18", platform: :ruby + gem "bundler-audit", "~> 0.9", require: false end plugins_path = File.join(File.dirname(__FILE__), "fastlane", "Pluginfile") diff --git a/app/Gemfile.lock b/app/Gemfile.lock index e1c7f3f8c..af6eb14f9 100644 --- a/app/Gemfile.lock +++ b/app/Gemfile.lock @@ -45,6 +45,9 @@ GEM base64 (0.2.0) benchmark (0.5.0) bigdecimal (4.0.1) + bundler-audit (0.9.3) + bundler (>= 1.2.0) + thor (~> 1.0) claide (1.1.0) cocoapods (1.16.2) addressable (~> 2.8) @@ -130,15 +133,16 @@ GEM faraday_middleware (1.2.1) faraday (~> 1.0) fastimage (2.4.0) - fastlane (2.230.0) + fastlane (2.232.1) CFPropertyList (>= 2.3, < 4.0.0) abbrev (~> 0.1.2) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) - aws-sdk-s3 (~> 1.0) + aws-sdk-s3 (~> 1.197) babosa (>= 1.0.3, < 2.0.0) base64 (~> 0.2.0) - bundler (>= 1.12.0, < 3.0.0) + benchmark (>= 0.1.0) + bundler (>= 1.17.3, < 5.0.0) colored (~> 1.2) commander (~> 4.6) csv (~> 3.3) @@ -153,7 +157,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-env (>= 1.6.0, <= 2.1.1) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -166,6 +170,7 @@ GEM naturally (~> 2.2) nkf (~> 0.2.0) optparse (>= 0.1.1, < 1.0.0) + ostruct (>= 0.1.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.5) @@ -186,38 +191,40 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.54.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.3) + google-apis-androidpublisher_v3 (0.96.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-core (0.18.0) addressable (~> 2.5, >= 2.5.1) - googleauth (>= 0.16.2, < 2.a) - httpclient (>= 2.8.1, < 3.a) + googleauth (~> 1.9) + httpclient (>= 2.8.3, < 3.a) mini_mime (~> 1.0) + mutex_m representable (~> 3.0) retriable (>= 2.0, < 4.a) - rexml - google-apis-iamcredentials_v1 (0.17.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-playcustomapp_v1 (0.13.0) - google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.31.0) - google-apis-core (>= 0.11.0, < 2.a) + google-apis-iamcredentials_v1 (0.26.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-playcustomapp_v1 (0.17.0) + google-apis-core (>= 0.15.0, < 2.a) + google-apis-storage_v1 (0.60.0) + google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (1.6.0) - faraday (>= 0.17.3, < 3.0) + google-cloud-env (2.1.1) + faraday (>= 1.0, < 3.a) google-cloud-errors (1.5.0) - google-cloud-storage (1.47.0) + google-cloud-storage (1.58.0) addressable (~> 2.8) digest-crc (~> 0.4) - google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.31.0) + google-apis-core (>= 0.18, < 2) + google-apis-iamcredentials_v1 (~> 0.18) + google-apis-storage_v1 (>= 0.42) google-cloud-core (~> 1.6) - googleauth (>= 0.16.2, < 2.a) + googleauth (~> 1.9) mini_mime (~> 1.0) - googleauth (1.8.1) - faraday (>= 0.17.3, < 3.a) + googleauth (1.11.2) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) @@ -253,6 +260,7 @@ GEM racc (~> 1.4) optparse (0.8.1) os (1.1.4) + ostruct (0.6.3) plist (3.7.2) prism (1.9.0) public_suffix (4.0.7) @@ -282,6 +290,7 @@ GEM terminal-notifier (2.0.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) + thor (1.5.0) trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.2) @@ -311,9 +320,10 @@ PLATFORMS DEPENDENCIES activesupport (>= 6.1.7.5, != 7.1.0) + bundler-audit (~> 0.9) cocoapods (>= 1.13, != 1.15.1, != 1.15.0) dotenv - fastlane (~> 2.230.0) + fastlane (~> 2.232.0) fastlane-plugin-increment_version_code fastlane-plugin-versioning_android nokogiri (~> 1.18) diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 0a78aa6ee..003f6eb2e 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -134,8 +134,8 @@ android { applicationId "com.proofofpassportapp" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 140 - versionName "2.9.15" + versionCode 142 + versionName "2.9.16" manifestPlaceholders = [appAuthRedirectScheme: 'com.proofofpassportapp'] externalNativeBuild { cmake { diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 2c777d48e..fb013cced 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -19,6 +19,8 @@ + + CFBundlePackageType APPL CFBundleShortVersionString - 2.9.15 + 2.9.16 CFBundleSignature ???? CFBundleURLTypes diff --git a/app/ios/Self.xcodeproj/project.pbxproj b/app/ios/Self.xcodeproj/project.pbxproj index 181d48af7..5450b6aed 100644 --- a/app/ios/Self.xcodeproj/project.pbxproj +++ b/app/ios/Self.xcodeproj/project.pbxproj @@ -547,7 +547,7 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/MoproKit/Libs", ); - MARKETING_VERSION = 2.9.15; + MARKETING_VERSION = 2.9.16; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -688,7 +688,7 @@ "$(PROJECT_DIR)", "$(PROJECT_DIR)/MoproKit/Libs", ); - MARKETING_VERSION = 2.9.15; + MARKETING_VERSION = 2.9.16; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/app/package.json b/app/package.json index 531105645..847c1e337 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "@selfxyz/mobile-app", - "version": "2.9.15", + "version": "2.9.16", "private": true, "type": "module", "scripts": { diff --git a/app/scripts/bundle-analyze-ci.cjs b/app/scripts/bundle-analyze-ci.cjs index f4f7dbe72..902aa4112 100755 --- a/app/scripts/bundle-analyze-ci.cjs +++ b/app/scripts/bundle-analyze-ci.cjs @@ -17,8 +17,8 @@ if (!platform || !['android', 'ios'].includes(platform)) { // Bundle size thresholds in MB - easy to update! const BUNDLE_THRESHOLDS_MB = { // TODO: fix temporary bundle bump - ios: 45, - android: 45, + ios: 46, + android: 46, }; function formatBytes(bytes) { diff --git a/app/src/assets/images/card_background_id1.png b/app/src/assets/images/card_background_id1.png new file mode 100644 index 000000000..5b17fa731 Binary files /dev/null and b/app/src/assets/images/card_background_id1.png differ diff --git a/app/src/assets/images/card_background_id2.png b/app/src/assets/images/card_background_id2.png new file mode 100644 index 000000000..19bb3ce75 Binary files /dev/null and b/app/src/assets/images/card_background_id2.png differ diff --git a/app/src/assets/images/card_background_id3.png b/app/src/assets/images/card_background_id3.png new file mode 100644 index 000000000..7b48bce3e Binary files /dev/null and b/app/src/assets/images/card_background_id3.png differ diff --git a/app/src/assets/images/card_background_id4.png b/app/src/assets/images/card_background_id4.png new file mode 100644 index 000000000..7781f399e Binary files /dev/null and b/app/src/assets/images/card_background_id4.png differ diff --git a/app/src/assets/images/card_background_id5.png b/app/src/assets/images/card_background_id5.png new file mode 100644 index 000000000..42771d941 Binary files /dev/null and b/app/src/assets/images/card_background_id5.png differ diff --git a/app/src/assets/images/card_background_id6.png b/app/src/assets/images/card_background_id6.png new file mode 100644 index 000000000..2735393ec Binary files /dev/null and b/app/src/assets/images/card_background_id6.png differ diff --git a/app/src/assets/images/dev_card_logo.svg b/app/src/assets/images/dev_card_logo.svg new file mode 100644 index 000000000..1742ab720 --- /dev/null +++ b/app/src/assets/images/dev_card_logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/app/src/assets/images/dev_card_wave.svg b/app/src/assets/images/dev_card_wave.svg new file mode 100644 index 000000000..3722fb1ad --- /dev/null +++ b/app/src/assets/images/dev_card_wave.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/assets/images/self_logo_inactive.svg b/app/src/assets/images/self_logo_inactive.svg new file mode 100644 index 000000000..65cccf8ce --- /dev/null +++ b/app/src/assets/images/self_logo_inactive.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/src/assets/images/self_logo_pending.svg b/app/src/assets/images/self_logo_pending.svg new file mode 100644 index 000000000..f28076f18 --- /dev/null +++ b/app/src/assets/images/self_logo_pending.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/src/assets/images/self_logo_unverified.svg b/app/src/assets/images/self_logo_unverified.svg new file mode 100644 index 000000000..67807f06a --- /dev/null +++ b/app/src/assets/images/self_logo_unverified.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/app/src/assets/images/wave_overlay.png b/app/src/assets/images/wave_overlay.png new file mode 100644 index 000000000..436c6fce1 Binary files /dev/null and b/app/src/assets/images/wave_overlay.png differ diff --git a/app/src/assets/images/wave_pattern_body.png b/app/src/assets/images/wave_pattern_body.png new file mode 100644 index 000000000..0d4c90de7 Binary files /dev/null and b/app/src/assets/images/wave_pattern_body.png differ diff --git a/app/src/assets/images/wave_pattern_pending.png b/app/src/assets/images/wave_pattern_pending.png new file mode 100644 index 000000000..4c86c41be Binary files /dev/null and b/app/src/assets/images/wave_pattern_pending.png differ diff --git a/app/src/assets/images/wave_pattern_transparent.png b/app/src/assets/images/wave_pattern_transparent.png new file mode 100644 index 000000000..7a9547a7c Binary files /dev/null and b/app/src/assets/images/wave_pattern_transparent.png differ diff --git a/app/src/components/homescreen/EmptyIdCard.tsx b/app/src/components/homescreen/EmptyIdCard.tsx new file mode 100644 index 000000000..0ae3dfeb5 --- /dev/null +++ b/app/src/components/homescreen/EmptyIdCard.tsx @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import type { FC } from 'react'; +import React from 'react'; +import { Image } from 'react-native'; +import { Text, XStack, YStack } from 'tamagui'; + +import { + black, + gray400, + slate200, + slate300, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import SelfLogoUnverified from '@/assets/images/self_logo_unverified.svg'; +import WavePatternBody from '@/assets/images/wave_pattern_body.png'; +import { cardStyles } from '@/components/homescreen/cardStyles'; +import { useCardDimensions } from '@/hooks/useCardDimensions'; + +interface EmptyIdCardProps { + onRegisterPress: () => void; +} + +/** + * Empty state card shown when user has no registered documents. + * Matches Figma design exactly: + * - White header with gray Self logo and "NO IDENTITY FOUND" text + * - Solid gray divider line + * - White body with gray wave pattern (from original unverified_human.png) + * - Pill-shaped white button with gray border + */ +const EmptyIdCard: FC = ({ onRegisterPress }) => { + const { + cardWidth, + borderRadius, + scale, + headerHeight, + figmaPadding, + logoSize, + headerGap, + expandedAspectRatio, + fontSize, + } = useCardDimensions(); + + return ( + + + {/* Header Section - White background with bottom border */} + + {/* Content row */} + + {/* Logo + Text */} + + {/* Self logo (gray) - exact Figma asset */} + + + + {/* Text container */} + + + NO IDENTITY FOUND + + + NO IDENTITY FOUND + + + + + + + {/* Body Section - White background with wave pattern */} + + {/* Wave pattern background - exact same as unverified_human.png */} + + + {/* Register button - pill-shaped with gray border */} + + + + Register a new ID + + + + + + + ); +}; + +export default EmptyIdCard; diff --git a/app/src/components/homescreen/ExpiredIdCard.tsx b/app/src/components/homescreen/ExpiredIdCard.tsx new file mode 100644 index 000000000..d315e03de --- /dev/null +++ b/app/src/components/homescreen/ExpiredIdCard.tsx @@ -0,0 +1,145 @@ +// SPDX-FileCopyrightText: 2025 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +import type { FC } from 'react'; +import React from 'react'; +import { Image } from 'react-native'; +import { Text, XStack, YStack } from 'tamagui'; + +import { + black, + gray400, + red600, + white, +} from '@selfxyz/mobile-sdk-alpha/constants/colors'; +import { dinot } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; + +import SelfLogoInactive from '@/assets/images/self_logo_inactive.svg'; +import WavePatternBody from '@/assets/images/wave_pattern_body.png'; +import { cardStyles } from '@/components/homescreen/cardStyles'; +import { useCardDimensions } from '@/hooks/useCardDimensions'; + +/** + * Expired state card shown when user's identity document has expired. + * Matches Figma design exactly: + * - White header with red Self logo and "EXPIRED ID" text + * - Red divider line + * - White body with gray wave pattern + * - Black "EXPIRED ID" badge in bottom right + */ +const ExpiredIdCard: FC = () => { + const { + cardWidth, + borderRadius, + scale, + headerHeight, + figmaPadding, + logoSize, + headerGap, + expandedAspectRatio, + fontSize, + } = useCardDimensions(); + + return ( + + + {/* Header Section - White background with red divider */} + + {/* Content row */} + + {/* Logo + Text */} + + {/* Red Self logo (reuses inactive logo) */} + + + + {/* Text container */} + + + EXPIRED ID + + + TIME TO REGISTER A VALID COPY + + + + + + + {/* Body Section - White background with wave pattern */} + + {/* Wave pattern background */} + + + {/* Expired badge - bottom right (black background) */} + + + EXPIRED ID + + + + + + ); +}; + +export default ExpiredIdCard; diff --git a/app/src/components/homescreen/IdCard.tsx b/app/src/components/homescreen/IdCard.tsx index 1a9cfbffc..afdf842ad 100644 --- a/app/src/components/homescreen/IdCard.tsx +++ b/app/src/components/homescreen/IdCard.tsx @@ -3,91 +3,439 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import type { FC } from 'react'; -import React from 'react'; -import { Dimensions } from 'react-native'; -import { Separator, Text, XStack, YStack } from 'tamagui'; +import React, { useCallback } from 'react'; +import { Image, Pressable, StyleSheet } from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; +import { Text, XStack, YStack } from 'tamagui'; +import { useNavigation } from '@react-navigation/native'; import type { AadhaarData } from '@selfxyz/common'; import type { PassportData } from '@selfxyz/common/types/passport'; -import { isAadhaarDocument, isMRZDocument } from '@selfxyz/common/utils/types'; +import type { KycData } from '@selfxyz/common/utils/types'; import { - black, - slate100, - slate300, - slate400, - slate500, + isAadhaarDocument, + isKycDocument, + isMRZDocument, +} from '@selfxyz/common/utils/types'; +import { WarningTriangleIcon } from '@selfxyz/euclid/dist/components/icons/WarningTriangleIcon'; +import { RoundFlag } from '@selfxyz/mobile-sdk-alpha/components'; +import { + red600, white, + yellow500, } from '@selfxyz/mobile-sdk-alpha/constants/colors'; import { dinot, plexMono } from '@selfxyz/mobile-sdk-alpha/constants/fonts'; -import AadhaarIcon from '@selfxyz/mobile-sdk-alpha/svgs/icons/aadhaar.svg'; -import EPassport from '@selfxyz/mobile-sdk-alpha/svgs/icons/epassport.svg'; -import LogoGray from '@/assets/images/logo_gray.svg'; -import { SvgXml } from '@/components/homescreen/SvgXmlWrapper'; -import { - formatDateFromYYMMDD, - getDocumentAttributes, - getNameAndSurname, -} from '@/utils/documentAttributes'; +import CardBackgroundId1 from '@/assets/images/card_background_id1.png'; +import CardBackgroundId2 from '@/assets/images/card_background_id2.png'; +import CardBackgroundId3 from '@/assets/images/card_background_id3.png'; +import CardBackgroundId4 from '@/assets/images/card_background_id4.png'; +import CardBackgroundId5 from '@/assets/images/card_background_id5.png'; +import CardBackgroundId6 from '@/assets/images/card_background_id6.png'; +import DevCardLogo from '@/assets/images/dev_card_logo.svg'; +import DevCardWave from '@/assets/images/dev_card_wave.svg'; +import SelfLogoPending from '@/assets/images/self_logo_pending.svg'; +import WaveOverlay from '@/assets/images/wave_overlay.png'; +import { getSecurityLevel } from '@/components/homescreen/cardSecurityBadge'; +import { cardStyles } from '@/components/homescreen/cardStyles'; +import KycIdCard from '@/components/homescreen/KycIdCard'; +import { useCardDimensions } from '@/hooks/useCardDimensions'; +import { getBackgroundIndex } from '@/utils/cardBackgroundSelector'; +import { getDocumentAttributes } from '@/utils/documentAttributes'; +import { registerModalCallbacks } from '@/utils/modalCallbackRegistry'; -// Import the logo SVG as a string -const logoSvg = ` - - - -`; +const CARD_BACKGROUNDS = [ + CardBackgroundId1, + CardBackgroundId2, + CardBackgroundId3, + CardBackgroundId4, + CardBackgroundId5, + CardBackgroundId6, +]; + +// Design tokens from Figma +const DEV_LOGO_BG = '#52525B'; // zinc/600 - grey circle background for dev logo +const DEV_BODY_COLOR = '#1E1B4B'; // indigo/950 - dev card body background + +// Country code to demonym mapping - comprehensive list for all supported countries +const COUNTRY_DEMONYMS: Record = { + // Major countries + USA: 'AMERICAN', + GBR: 'BRITISH', + JPN: 'JAPANESE', + DEU: 'GERMAN', + 'D<<': 'GERMAN', // German passports use D<< + FRA: 'FRENCH', + CAN: 'CANADIAN', + IND: 'INDIAN', + AUS: 'AUSTRALIAN', + NGA: 'NIGERIAN', + FIN: 'FINNISH', + ITA: 'ITALIAN', + ESP: 'SPANISH', + BRA: 'BRAZILIAN', + MEX: 'MEXICAN', + CHN: 'CHINESE', + KOR: 'SOUTH KOREAN', + PRK: 'NORTH KOREAN', + NLD: 'DUTCH', + SWE: 'SWEDISH', + NOR: 'NORWEGIAN', + DNK: 'DANISH', + CHE: 'SWISS', + AUT: 'AUSTRIAN', + BEL: 'BELGIAN', + PRT: 'PORTUGUESE', + GRC: 'GREEK', + POL: 'POLISH', + IRL: 'IRISH', + NZL: 'NEW ZEALANDER', + ZAF: 'SOUTH AFRICAN', + SGP: 'SINGAPOREAN', + MYS: 'MALAYSIAN', + THA: 'THAI', + PHL: 'FILIPINO', + IDN: 'INDONESIAN', + VNM: 'VIETNAMESE', + ARE: 'EMIRATI', + SAU: 'SAUDI', + ISR: 'ISRAELI', + EGY: 'EGYPTIAN', + TUR: 'TURKISH', + RUS: 'RUSSIAN', + UKR: 'UKRAINIAN', + ARG: 'ARGENTINIAN', + COL: 'COLOMBIAN', + CHL: 'CHILEAN', + PER: 'PERUVIAN', + // Europe + ALB: 'ALBANIAN', + AND: 'ANDORRAN', + ARM: 'ARMENIAN', + AZE: 'AZERBAIJANI', + BLR: 'BELARUSIAN', + BIH: 'BOSNIAN', + BGR: 'BULGARIAN', + HRV: 'CROATIAN', + CYP: 'CYPRIOT', + CZE: 'CZECH', + EST: 'ESTONIAN', + GEO: 'GEORGIAN', + HUN: 'HUNGARIAN', + ISL: 'ICELANDIC', + LVA: 'LATVIAN', + LIE: 'LIECHTENSTEINER', + LTU: 'LITHUANIAN', + LUX: 'LUXEMBOURGISH', + MLT: 'MALTESE', + MDA: 'MOLDOVAN', + MCO: 'MONACAN', + MNE: 'MONTENEGRIN', + MKD: 'MACEDONIAN', + ROU: 'ROMANIAN', + SMR: 'SAMMARINESE', + SRB: 'SERBIAN', + SVK: 'SLOVAK', + SVN: 'SLOVENIAN', + VAT: 'VATICAN', + // Americas + ATG: 'ANTIGUAN', + BHS: 'BAHAMIAN', + BRB: 'BARBADIAN', + BLZ: 'BELIZEAN', + BOL: 'BOLIVIAN', + CRI: 'COSTA RICAN', + CUB: 'CUBAN', + DMA: 'DOMINICAN', + DOM: 'DOMINICAN', + ECU: 'ECUADORIAN', + SLV: 'SALVADORAN', + GRD: 'GRENADIAN', + GTM: 'GUATEMALAN', + GUY: 'GUYANESE', + HTI: 'HAITIAN', + HND: 'HONDURAN', + JAM: 'JAMAICAN', + NIC: 'NICARAGUAN', + PAN: 'PANAMANIAN', + PRY: 'PARAGUAYAN', + KNA: 'KITTITIAN', + LCA: 'SAINT LUCIAN', + VCT: 'VINCENTIAN', + SUR: 'SURINAMESE', + TTO: 'TRINIDADIAN', + URY: 'URUGUAYAN', + VEN: 'VENEZUELAN', + // Africa + DZA: 'ALGERIAN', + AGO: 'ANGOLAN', + BEN: 'BENINESE', + BWA: 'BOTSWANAN', + BFA: 'BURKINABE', + BDI: 'BURUNDIAN', + CPV: 'CAPE VERDEAN', + CMR: 'CAMEROONIAN', + CAF: 'CENTRAL AFRICAN', + TCD: 'CHADIAN', + COM: 'COMORIAN', + COG: 'CONGOLESE', + COD: 'CONGOLESE', + CIV: 'IVORIAN', + DJI: 'DJIBOUTIAN', + GNQ: 'EQUATOGUINEAN', + ERI: 'ERITREAN', + SWZ: 'SWAZI', + ETH: 'ETHIOPIAN', + GAB: 'GABONESE', + GMB: 'GAMBIAN', + GHA: 'GHANAIAN', + GIN: 'GUINEAN', + GNB: 'BISSAU-GUINEAN', + KEN: 'KENYAN', + LSO: 'BASOTHO', + LBR: 'LIBERIAN', + LBY: 'LIBYAN', + MDG: 'MALAGASY', + MWI: 'MALAWIAN', + MLI: 'MALIAN', + MRT: 'MAURITANIAN', + MUS: 'MAURITIAN', + MAR: 'MOROCCAN', + MOZ: 'MOZAMBICAN', + NAM: 'NAMIBIAN', + NER: 'NIGERIEN', + RWA: 'RWANDAN', + STP: 'SAO TOMEAN', + SEN: 'SENEGALESE', + SYC: 'SEYCHELLOIS', + SLE: 'SIERRA LEONEAN', + SOM: 'SOMALI', + SSD: 'SOUTH SUDANESE', + SDN: 'SUDANESE', + TZA: 'TANZANIAN', + TGO: 'TOGOLESE', + TUN: 'TUNISIAN', + UGA: 'UGANDAN', + ZMB: 'ZAMBIAN', + ZWE: 'ZIMBABWEAN', + // Asia & Middle East + AFG: 'AFGHAN', + BHR: 'BAHRAINI', + BGD: 'BANGLADESHI', + BTN: 'BHUTANESE', + BRN: 'BRUNEIAN', + KHM: 'CAMBODIAN', + TWN: 'TAIWANESE', + HKG: 'HONG KONGER', + IRQ: 'IRAQI', + IRN: 'IRANIAN', + JOR: 'JORDANIAN', + KAZ: 'KAZAKHSTANI', + KWT: 'KUWAITI', + KGZ: 'KYRGYZSTANI', + LAO: 'LAOTIAN', + LBN: 'LEBANESE', + MAC: 'MACANESE', + MDV: 'MALDIVIAN', + MNG: 'MONGOLIAN', + MMR: 'MYANMAR', + NPL: 'NEPALI', + OMN: 'OMANI', + PAK: 'PAKISTANI', + PSE: 'PALESTINIAN', + QAT: 'QATARI', + LKA: 'SRI LANKAN', + SYR: 'SYRIAN', + TJK: 'TAJIKISTANI', + TKM: 'TURKMEN', + UZB: 'UZBEKISTANI', + YEM: 'YEMENI', + // Oceania + FJI: 'FIJIAN', + KIR: 'I-KIRIBATI', + MHL: 'MARSHALLESE', + FSM: 'MICRONESIAN', + NRU: 'NAURUAN', + PLW: 'PALAUAN', + PNG: 'PAPUA NEW GUINEAN', + WSM: 'SAMOAN', + SLB: 'SOLOMON ISLANDER', + TON: 'TONGAN', + TUV: 'TUVALUAN', + VUT: 'NI-VANUATU', + TLS: 'TIMORESE', +}; + +/** + * Get country demonym from 3-letter country code. + * Falls back to the code itself if no mapping exists. + * Note: D<< (German passports) should be normalized to DEU before calling this. + */ +const getCountryDemonym = (code: string): string => { + if (!code) return ''; + const upperCode = code.toUpperCase().replace(/ = ({ idDocument, selected, hidden, + isInactive = false, }) => { + const navigation = useNavigation(); + const navigateToDocumentOnboarding = useCallback(() => { + switch (idDocument?.documentCategory) { + case 'passport': + case 'id_card': + navigation.navigate('DocumentOnboarding'); + break; + case 'aadhaar': + navigation.navigate('AadhaarUpload', { countryCode: 'IND' }); + break; + } + }, [idDocument?.documentCategory, navigation]); + + const handleInactivePress = useCallback(() => { + const callbackId = registerModalCallbacks({ + onButtonPress: navigateToDocumentOnboarding, + onModalDismiss: () => {}, + }); + + navigation.navigate('Modal', { + titleText: 'Your ID needs to be reactivated to continue', + bodyText: + 'Make sure that you have your document and recovery method ready.', + buttonText: 'Continue', + secondaryButtonText: 'Not now', + callbackId, + }); + }, [navigateToDocumentOnboarding, navigation]); + // Early return if document is null + // Call hooks at the top, before any conditional returns + const { + cardWidth, + cardHeight, + borderRadius, + scale, + headerHeight, + figmaPadding, + logoSize, + headerGap, + fontSize, + } = useCardDimensions(selected); + if (!idDocument) { return null; } - // Function to mask MRZ characters except '<' and spaces - const maskMrzValue = (text: string): string => { - return text.replace(/./g, 'X'); + // KYC documents use a distinct dark card design + if (isKycDocument(idDocument)) { + return ( +