├── .nvmrc ├── .java-version ├── yarn-audit-known-issues ├── .gitattributes ├── .vscode ├── launch.json ├── extensions.json └── settings.json ├── tsconfig.eslint.json ├── assets ├── icon │ ├── icon.png │ ├── adaptive-background.png │ └── adaptive-foreground.png ├── background.jpg ├── splash │ ├── hdpi.jpg │ ├── mdpi.jpg │ ├── xhdpi.jpg │ ├── xxhdpi.jpg │ └── xxxhdpi.jpg ├── background@2x.jpg ├── background@3x.jpg ├── background@4x.jpg ├── background@1.5x.jpg ├── fonts │ ├── Inter-Bold.ttf │ ├── Inter-Medium.ttf │ ├── Inter-Regular.ttf │ └── Inter-SemiBold.ttf ├── intro-background.png ├── intro-background@2x.png ├── intro-background@3x.png ├── intro-background@4x.png ├── intro-background@1.5x.png ├── Logo.tsx └── WelcomeLogo.tsx ├── babel.config.js ├── .watchmanconfig ├── .editorconfig ├── metro.config.js ├── .github ├── scripts │ ├── tsconfig.json │ ├── createAppVersionPr.ts │ ├── enableAutomergeOnPr.js │ ├── autoApprovePr.js │ └── uploadE2eBuildToEmerge.ts ├── CODEOWNERS ├── CONTRIBUTING.md ├── workflows │ ├── e2e-pr.yml │ ├── semantic-pr.yml │ ├── e2e-ios.yml │ ├── e2e-main.yml │ ├── bump-app-version.yml │ ├── e2e-android.yml │ ├── release-nightly.yml │ └── check.yml ├── ISSUE_TEMPLATE │ ├── 2-feature-request.md │ ├── 3-task.md │ ├── config.yml │ └── 1-bug-report.md ├── pull_request_template.md └── actions │ └── yarn-install │ └── action.yml ├── tsconfig.json ├── locales ├── zh-CN.json ├── th-TH.json ├── en-US.json ├── vi-VN.json ├── de.json ├── tr-TR.json ├── ru-RU.json ├── uk-UA.json ├── es-419.json ├── it-IT.json ├── pt-BR.json ├── pl-PL.json └── fr-FR.json ├── .env.example ├── codecov.yml ├── scripts ├── ci_check_vulnerabilities.sh ├── push.sh ├── sync-eas-node-version.ts └── generate-release-notes.ts ├── .eslintrc.js ├── plugins ├── withCustomGradleProperties.js ├── withDesugaring.js └── withAndroidAppThemeFullScreen.js ├── knip.ts ├── eas.json ├── .gitignore ├── RELEASES.md ├── docs ├── connecting-dapps.md ├── deeplinks.md └── watching-assets.mdx ├── .easignore ├── README.md ├── renovate.json5 ├── index.tsx ├── CONTRIBUTING.md ├── SECURITY.md ├── package.json ├── app.config.js ├── LICENSE └── WALLET.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 24.11.1 2 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 17.0 2 | -------------------------------------------------------------------------------- /yarn-audit-known-issues: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": [] 4 | } 5 | -------------------------------------------------------------------------------- /assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/icon/icon.png -------------------------------------------------------------------------------- /assets/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/background.jpg -------------------------------------------------------------------------------- /assets/splash/hdpi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/splash/hdpi.jpg -------------------------------------------------------------------------------- /assets/splash/mdpi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/splash/mdpi.jpg -------------------------------------------------------------------------------- /assets/background@2x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/background@2x.jpg -------------------------------------------------------------------------------- /assets/background@3x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/background@3x.jpg -------------------------------------------------------------------------------- /assets/background@4x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/background@4x.jpg -------------------------------------------------------------------------------- /assets/splash/xhdpi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/splash/xhdpi.jpg -------------------------------------------------------------------------------- /assets/splash/xxhdpi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/splash/xxhdpi.jpg -------------------------------------------------------------------------------- /assets/splash/xxxhdpi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/splash/xxxhdpi.jpg -------------------------------------------------------------------------------- /assets/background@1.5x.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/background@1.5x.jpg -------------------------------------------------------------------------------- /assets/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /assets/intro-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/intro-background.png -------------------------------------------------------------------------------- /assets/fonts/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/fonts/Inter-Medium.ttf -------------------------------------------------------------------------------- /assets/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /assets/intro-background@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/intro-background@2x.png -------------------------------------------------------------------------------- /assets/intro-background@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/intro-background@3x.png -------------------------------------------------------------------------------- /assets/intro-background@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/intro-background@4x.png -------------------------------------------------------------------------------- /assets/fonts/Inter-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/fonts/Inter-SemiBold.ttf -------------------------------------------------------------------------------- /assets/intro-background@1.5x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/intro-background@1.5x.png -------------------------------------------------------------------------------- /assets/icon/adaptive-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/icon/adaptive-background.png -------------------------------------------------------------------------------- /assets/icon/adaptive-foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/valora-inc/wallet/HEAD/assets/icon/adaptive-foreground.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | 'react-native-reanimated/plugin', // NOTE: this plugin MUST be last 7 | ], 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "__comment": "On macOS the first 8 items listed in ignore_dirs can be accelerated at the OS level. See https://facebook.github.io/watchman/docs/config.html#ignore_dirs", 3 | "ignore_dirs": ["android/app/build", "ios/build", "e2e/artifacts"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true -------------------------------------------------------------------------------- /metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require('@divvi/mobile/metro-config') 3 | const { withSentryConfig } = require('@sentry/react-native/metro') 4 | 5 | /** @type {import('expo/metro-config').MetroConfig} */ 6 | const config = withSentryConfig(getDefaultConfig(__dirname)) 7 | 8 | module.exports = config 9 | -------------------------------------------------------------------------------- /.github/scripts/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es2020"], 4 | "skipLibCheck": true, 5 | "moduleResolution": "node", 6 | "module": "commonjs", 7 | "esModuleInterop": true, 8 | "resolveJsonModule": true, 9 | "allowJs": true, 10 | "checkJs": true, 11 | "noEmit": true, 12 | "strict": true 13 | }, 14 | "include": ["*.js", "*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | # For details on acceptable file patterns, please refer to the [Github Documentation](https://help.github.com/articles/about-codeowners/) 4 | 5 | # default owners, overridden by package specific owners below 6 | * @valora-inc/wallet 7 | 8 | # directory and file-level owners. Feel free to add to this! 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@divvi/mobile/tsconfig.base", 3 | "compilerOptions": { 4 | "resolveJsonModule": true 5 | }, 6 | // TODO: we should be able to remove the need to include the mobile package index.d.ts once we're able to ship declaration files 7 | "files": [], 8 | "include": ["**/*", "./node_modules/@divvi/mobile/src/index.d.ts"], 9 | // Ad hoc fix for typecheck, due to knip being ESM 10 | "exclude": ["knip.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "连接相机可以扫描代码进行付款。", 4 | "NSContactsUsageDescription": "添加您的联系人可以轻松地向您的朋友发送和请求付款。", 5 | "NSPhotoLibraryAddUsageDescription": "连接您的照片库可以将代码保存到您的照片中。", 6 | "NSPhotoLibraryUsageDescription": "选择个人资料图片需要执行此操作。", 7 | "NSUserTrackingUsageDescription": "我们使用广告标识符来准确地将应用程序安装归因到广告活动。", 8 | "NSFaceIDUsageDescription": "这是您使用面容 ID 保护您的账户所必需的。", 9 | "NSLocationWhenInUseUsageDescription": "此应用程序需要位置访问才能提供基于位置的功能。" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | EXPO_PUBLIC_ALCHEMY_API_KEY= 2 | EXPO_PUBLIC_BIDALI_URL= 3 | EXPO_PUBLIC_SEGMENT_API_KEY= 4 | EXPO_PUBLIC_SENTRY_CLIENT_URL= 5 | EXPO_PUBLIC_STATSIG_API_KEY= 6 | EXPO_PUBLIC_WALLET_CONNECT_PROJECT_ID= 7 | EXPO_PUBLIC_ZENDESK_API_KEY= 8 | GOOGLE_SERVICES_JSON= 9 | GOOGLE_SERVICE_INFO_PLIST= 10 | SENTRY_AUTH_TOKEN= -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for considering making a contribution to the Celo community! 4 | Everyone is encouraged to contribute, even the smallest fixes are welcome. 5 | 6 | If you'd like to contribute to Celo, please fork, fix, commit and send a 7 | pull request for the maintainers to review. 8 | 9 | If you wish to submit more complex changes, please sync with a core developer first. 10 | This will help ensure those changes are in line with the general philosophy of the project 11 | and enable you to get some early feedback. 12 | 13 | See the [contributing guide](https://docs.celo.org/community/contributing) for details on how to participate. 14 | -------------------------------------------------------------------------------- /.github/workflows/e2e-pr.yml: -------------------------------------------------------------------------------- 1 | name: E2E - PR 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | merge_group: 7 | 8 | # Cancel any in progress run of the workflow for a given PR 9 | # This avoids building outdated code 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | android: 16 | name: Android 17 | uses: ./.github/workflows/e2e-android.yml 18 | with: 19 | android-api-level: 34 20 | secrets: inherit 21 | ios: 22 | name: iOS 23 | uses: ./.github/workflows/e2e-ios.yml 24 | with: 25 | ios-version: '15.2' 26 | secrets: inherit 27 | -------------------------------------------------------------------------------- /.github/workflows/semantic-pr.yml: -------------------------------------------------------------------------------- 1 | name: Semantic PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - reopened 8 | - edited 9 | - synchronize 10 | merge_group: 11 | 12 | jobs: 13 | semantic-pr-title: 14 | name: Semantic PR title 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | # This action can only be run in `pull_request_target` and `pull_request` events 21 | # Workaround from https://github.com/amannn/action-semantic-pull-request/issues/236#issuecomment-1695654373 22 | if: ${{ github.event_name != 'merge_group' }} 23 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: main 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: '75...100' # Range in which checks are green 8 | status: 9 | project: 10 | default: 11 | target: 75% # the required coverage value 12 | threshold: 5% # the leniency in hitting the target 13 | base: auto 14 | patch: 15 | default: 16 | target: 75% # the required coverage value 17 | threshold: 5% # the leniency in hitting the target 18 | base: auto 19 | 20 | parsers: 21 | gcov: 22 | branch_detection: 23 | conditional: yes 24 | loop: yes 25 | method: no 26 | macro: no 27 | 28 | comment: 29 | layout: 'reach,diff,flags,files,footer' 30 | behavior: default 31 | require_changes: no 32 | -------------------------------------------------------------------------------- /locales/th-TH.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "การเชื่อมต่อกล้องของคุณจะช่วยให้คุณสามารถสแกนรหัสเพื่อชำระเงินได้", 4 | "NSContactsUsageDescription": "การเพิ่มผู้ติดต่อของคุณจะช่วยให้การส่งและส่งคำขอการชำระเงินกับเพื่อนของคุณเป็นเรื่องง่าย", 5 | "NSPhotoLibraryAddUsageDescription": "การเชื่อมต่อไลบรารี่รูปภาพของคุณจะช่วยให้คุณสามารถบันทึกรหัสของคุณเป็นรูปภาพได้", 6 | "NSPhotoLibraryUsageDescription": "คุณจะต้องดำเนินการนี้เพื่อเลือกรูปโปรไฟล์ของคุณ", 7 | "NSUserTrackingUsageDescription": "เราใช้รหัสโฆษณาเพื่อกระจายการติดตั้งแอปจากแคมเปญโฆษณา", 8 | "NSFaceIDUsageDescription": "คุณจะต้องดำเนินการนี้เพื่อใช้ Face ID รักษาความปลอดภัยบัญชีของคุณ", 9 | "NSLocationWhenInUseUsageDescription": "แอปนี้ต้องการการเข้าถึงตำแหน่งจึงจะสามารถให้บริการฟีเจอร์ตามตำแหน่งได้" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /scripts/ci_check_vulnerabilities.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # workaround for missing feature 4 | # https://github.com/yarnpkg/yarn/issues/6669 5 | 6 | set -u 7 | 8 | set +e 9 | output=$(yarn audit --json --groups dependencies --level high) 10 | result=$? 11 | set -e 12 | 13 | if [ $result -eq 0 ]; then 14 | # everything is fine 15 | exit 0 16 | fi 17 | 18 | if [ -f yarn-audit-known-issues ] && echo "$output" | grep auditAdvisory | diff -q yarn-audit-known-issues - > /dev/null 2>&1; then 19 | echo 20 | echo Ignorning known vulnerabilities 21 | exit 0 22 | fi 23 | 24 | echo 25 | echo Security vulnerabilities were found that were not ignored. 26 | echo See https://github.com/valora-inc/wallet/tree/main/WALLET.md#vulnerabilities-found-in-dependencies 27 | echo 28 | echo "$output" | grep auditAdvisory | jq 29 | 30 | exit "$result" -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "eamodio.gitlens", 8 | "esbenp.prettier-vscode", 9 | "juanblanco.solidity", 10 | "redhat.vscode-yaml", 11 | "smkamranqadri.vscode-bolt-language", 12 | "pkief.material-icon-theme", 13 | "davidanson.vscode-markdownlint", 14 | "mikestead.dotenv", 15 | "coenraads.bracket-pair-colorizer-2", 16 | "dbaeumer.vscode-eslint" 17 | ], 18 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 19 | "unwantedRecommendations": [] 20 | } 21 | -------------------------------------------------------------------------------- /locales/en-US.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Connecting your camera allows you to scan codes for payments.", 4 | "NSContactsUsageDescription": "Adding your contacts makes it easy to send and request payments with your friends.", 5 | "NSPhotoLibraryAddUsageDescription": "Connecting your photo library allows you to save your code to your photos.", 6 | "NSPhotoLibraryUsageDescription": "This is required for you to choose a profile picture.", 7 | "NSUserTrackingUsageDescription": "We use the advertising identifier to accurately attribute app installs from ad campaigns.", 8 | "NSFaceIDUsageDescription": "This is required for you to use Face ID to secure your account.", 9 | "NSLocationWhenInUseUsageDescription": "This app requires location access to provide location-based features." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /locales/vi-VN.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Bằng cách kết nối camera, bạn có thể quét mã để thanh toán.", 4 | "NSContactsUsageDescription": "Khi thêm danh bạ, bạn có thể dễ dàng gửi và yêu cầu thanh toán với bạn bè.", 5 | "NSPhotoLibraryAddUsageDescription": "Bằng cách kết nối thư viện ảnh, bạn có thể lưu mã vào danh sách ảnh của mình.", 6 | "NSPhotoLibraryUsageDescription": "Đây là yêu cầu bắt buộc để bạn có thể chọn ảnh hồ sơ.", 7 | "NSUserTrackingUsageDescription": "Chúng tôi sử dụng mã nhận dạng cho quảng cáo để phân bổ chính xác lượt cài đặt ứng dụng từ các chiến dịch quảng cáo.", 8 | "NSFaceIDUsageDescription": "Đây là yêu cầu bắt buộc để bạn sử dụng Face ID nhằm bảo vệ tài khoản.", 9 | "NSLocationWhenInUseUsageDescription": "Ứng dụng này yêu cầu quyền truy cập vị trí để cung cấp các tính năng dựa trên vị trí." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Wenn du deine Kamera verbindest, kannst du Codes für Zahlungen scannen.", 4 | "NSContactsUsageDescription": "Das Hinzufügen deiner Kontakte macht es einfach, Zahlungen an deine Freunde zu senden und anzufordern.", 5 | "NSPhotoLibraryAddUsageDescription": "Wenn du deine Foto-Bibliothek anschließt, kannst du deinen Code mit deinen Fotos speichern.", 6 | "NSPhotoLibraryUsageDescription": "Dies ist erforderlich, um ein Profilbild auszuwählen.", 7 | "NSUserTrackingUsageDescription": "Wir verwenden die Werbe-Identifikation, um App-Installationen aus Werbekampagnen genau zuzuordnen.", 8 | "NSFaceIDUsageDescription": "Dies ist erforderlich, um Face ID auszuwählen.", 9 | "NSLocationWhenInUseUsageDescription": "Diese App erfordert Standortzugriff, um standortbasierte Funktionen bereitzustellen." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /locales/tr-TR.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Kamera izni ödemeler için kodları taramanıza olanak sağlar.", 4 | "NSContactsUsageDescription": "Kişilerinizi eklemeniz, arkadaşlarınıza ödeme göndermeyi ve ödeme talebinde bulunmayı kolaylaştırır.", 5 | "NSPhotoLibraryAddUsageDescription": "Fotoğraf kitaplığınızı bağlamak, kodunuzu fotoğraflarınıza kaydetmenize olanak tanır.", 6 | "NSPhotoLibraryUsageDescription": "Profil resmi seçmeniz için gereklidir.", 7 | "NSUserTrackingUsageDescription": "Reklam kampanyaları ile uygulama yüklemelerini ilişkilendirmek için reklam tanımlayıcı kullanılmaktadır.", 8 | "NSFaceIDUsageDescription": "Hesabınızın güvenliğini sağlamak için Face ID kullanmanız gereklidir.", 9 | "NSLocationWhenInUseUsageDescription": "Bu uygulamanın konum tabanlı özellikler sunabilmesi için konum erişimine ihtiyacı var." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '✨ Feature' 3 | about: Suggest a feature or enhancement to improve Valora. 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | 16 | 17 | ### What would you like? 18 | 19 | 20 | 21 | ### Why is this needed? 22 | 23 | 24 | -------------------------------------------------------------------------------- /locales/ru-RU.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Подключение камеры позволяет сканировать коды для оплаты.", 4 | "NSContactsUsageDescription": "Добавление контактов позволит легко отправлять или запрашивать платежи.", 5 | "NSPhotoLibraryAddUsageDescription": "Подключение библиотеки фотографий позволит вам сохранять свой код на фотографиях.", 6 | "NSPhotoLibraryUsageDescription": "Это необходимо для выбора изображения профиля.", 7 | "NSUserTrackingUsageDescription": "Мы используем идентификатор рекламы для точного определения установочных приложений из рекламных кампаний.", 8 | "NSFaceIDUsageDescription": "Для защиты вашего аккаунта необходимо использовать функцию разблокировки по лицу.", 9 | "NSLocationWhenInUseUsageDescription": "Для предоставления функций, основанных на местоположении, этому приложению требуется доступ к местоположению." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /locales/uk-UA.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Під’єднання вашої камери дозволяє сканувати коди для платежів.", 4 | "NSContactsUsageDescription": "Додавання ваших контактів полегшує надсилання та запит платежів із друзями.", 5 | "NSPhotoLibraryAddUsageDescription": "Під’єднання вашої бібліотеки фотографій дозволяє зберегти код до ваших фото.", 6 | "NSPhotoLibraryUsageDescription": "Вам потрібно вибрати зображення профілю.", 7 | "NSUserTrackingUsageDescription": "Ми використовуємо ідентифікатор реклами для точної атрибуції інсталяцій програмних застосунків від рекламних кампаній.", 8 | "NSFaceIDUsageDescription": "Вам необхідно використовувати ідентифікатор обличчя для захисту вашого облікового запису.", 9 | "NSLocationWhenInUseUsageDescription": "Цій програмі потрібен доступ до місцезнаходження, щоб надавати функції на основі місцезнаходження." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /locales/es-419.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Conectar tu cámara te permite escanear códigos para hacer pagos.", 4 | "NSContactsUsageDescription": "Conectar tu lista de contactos hace que enviar y solicitar pagos a tus amigos sea aún más fácil.", 5 | "NSPhotoLibraryAddUsageDescription": "Conectar tu galería de fotos te permite guardar tu código en tus fotos.", 6 | "NSPhotoLibraryUsageDescription": "También es necesario para poder elegir una foto de perfil.", 7 | "NSUserTrackingUsageDescription": "Utilizamos el identificador de publicidad para detectar las instalaciones ligadas a campañas publicitarias.", 8 | "NSFaceIDUsageDescription": "También es necesario para habilitar Face ID como método de seguridad.", 9 | "NSLocationWhenInUseUsageDescription": "Esta aplicación requiere acceso a la ubicación para proporcionar funciones basadas en la ubicación." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /locales/it-IT.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Collegare la fotocamera puoi leggere i codici necessari per pagamenti.", 4 | "NSContactsUsageDescription": "Aggiungendo i tuoi contatti puoi inviare e richiedere facilmente pagamenti ai tuoi amici.", 5 | "NSPhotoLibraryAddUsageDescription": "Collegando la tua raccolta di foto puoi salvare il codice nelle tue foto.", 6 | "NSPhotoLibraryUsageDescription": "Questo è necessario per scegliere un’immagine del profilo.", 7 | "NSUserTrackingUsageDescription": "Utilizziamo l’identificatore di pubblicità per attribuire con precisione installazioni di app da campagne pubblicitarie.", 8 | "NSFaceIDUsageDescription": "Questo è necessario per utilizzare Face ID per proteggere il tuo conto.", 9 | "NSLocationWhenInUseUsageDescription": "Questa app richiede l'accesso alla posizione per fornire funzionalità basate sulla posizione." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '📝 Task' 3 | about: Create a new epic or issue 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | ### Description 12 | 13 | 14 | 15 | ### User Stories 16 | 17 | 18 | 19 | ### Acceptance Criteria 20 | 21 | 22 | -------------------------------------------------------------------------------- /locales/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Conectar a sua câmera permite que você possa escanear códigos de pagamento.", 4 | "NSContactsUsageDescription": "Adicionar os seus contatos torna fácil enviar e solicitar pagamentos a seus amigos.", 5 | "NSPhotoLibraryAddUsageDescription": "Conectar a sua galeria de fotos nos permite salvar seu código junto às suas fotos.", 6 | "NSPhotoLibraryUsageDescription": "É necessária permissão para escolher uma foto de perfil.", 7 | "NSUserTrackingUsageDescription": "Usamos o identificador de publicidade para atribuir com precisão as instalações de aplicativos das campanhas publicitárias.", 8 | "NSFaceIDUsageDescription": "É necessária permissão para usar o Face ID para garantir sua conta.", 9 | "NSLocationWhenInUseUsageDescription": "Este aplicativo requer acesso à localização para fornecer recursos baseados em localização." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /locales/pl-PL.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "Podłączenie aparatu umożliwia skanowanie kodów w celu przesyłania płatności.", 4 | "NSContactsUsageDescription": "Dodanie kontaktów ułatwi Tobie i Twoim znajomym wzajemne przesyłanie i odbieranie płatności.", 5 | "NSPhotoLibraryAddUsageDescription": "Podłączenie biblioteki zdjęć pozwala na zapisanie kodu do zdjęć.", 6 | "NSPhotoLibraryUsageDescription": "Jest to wymagane, aby użytkownik mógł wybrać zdjęcie profilowe.", 7 | "NSUserTrackingUsageDescription": "Używamy identyfikatora reklamowego w celu dokładnego przypisania instalacji aplikacji z kampanii reklamowych.", 8 | "NSFaceIDUsageDescription": "Jest to wymagane, aby użytkownik mógł korzystać z technologii Face ID w celu zabezpieczenia konta.", 9 | "NSLocationWhenInUseUsageDescription": "Ta aplikacja wymaga dostępu do lokalizacji, aby móc oferować funkcje oparte na lokalizacji." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 5 | 6 | ### Test plan 7 | 8 | 11 | 12 | ### Related issues 13 | 14 | - Fixes #[issue number here] 15 | 16 | ### Backwards compatibility 17 | 18 | 19 | 20 | ### Network scalability 21 | 22 | If a new NetworkId and/or Network are added in the future, the changes in this PR will: 23 | 24 | - [ ] Continue to work without code changes, OR trigger a compilation error (guaranteeing we find it when a new network is added) 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@valora/eslint-config-typescript'], 3 | plugins: ['jsx-expressions'], 4 | parserOptions: { 5 | project: './tsconfig.eslint.json', 6 | }, 7 | ignorePatterns: ['**/__mocks__/**', '**/lcov-report/**', 'vendor', '.bundle'], 8 | rules: { 9 | // Maybe move it to @valora/eslint-config-typescript? 10 | 'jest/valid-title': ['error', { ignoreTypeOfDescribeName: true }], 11 | 'no-console': ['error', { allow: [''] }], 12 | }, 13 | overrides: [ 14 | { 15 | files: ['./**/*.ts', './**/*.tsx'], 16 | excludedFiles: ['./**/*.test.ts', './**/*.test.tsx'], 17 | rules: { 18 | 'jsx-expressions/strict-logical-expressions': 'error', 19 | // Turning off this rule because we don't have jest in this project yet 20 | // and it complains about not being able to find the version of jest 21 | // TODO: remove this once we add jest in this project, 22 | 'jest/no-deprecated-functions': 'off', 23 | }, 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /locales/fr-FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "ios": { 3 | "NSCameraUsageDescription": "En connectant votre caméra vous pourrez scanner des codes pour les paiements.", 4 | "NSContactsUsageDescription": "L'ajout de vos contacts facilite l'envoi et la demande de paiements avec vos amis.", 5 | "NSPhotoLibraryAddUsageDescription": "La connexion à votre galerie photos vous permet d'enregistrer votre code dans vos photos.", 6 | "NSPhotoLibraryUsageDescription": "Cette action est nécessaire pour vous permettre de choisir une photo de profil.", 7 | "NSUserTrackingUsageDescription": "Nous utilisons l'identifiant de la publicité pour attribuer avec précision les téléchargements de l'appli directement imputables à nos campagnes publicitaires.", 8 | "NSFaceIDUsageDescription": "Cette action est nécessaire pour vous permettre d'utiliser Face ID pour sécuriser ton compte.", 9 | "NSLocationWhenInUseUsageDescription": "Cette application nécessite un accès à la localisation pour fournir des fonctionnalités basées sur la localisation." 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 🙋 Get Support for your issues 4 | url: https://valoraapp.com/support 5 | about: This issue tracker is not for support questions. Please refer to our support page or submit an in-app support ticket. 6 | - name: 📲 Download Valora 7 | url: https://valoraapp.com/ 8 | about: Visit to download Valora on a mobile device. 9 | - name: 💬 Join the Discussion on Discord 10 | url: https://discord.gg/7tKnCbHv5j 11 | about: This issue tracker is not for external development discussions. Please join our Discord to talk about developing with Valora for external apps or the CELO protocol. 12 | - name: The CELO Website 13 | url: https://celo.org/ 14 | about: The Official Celo Website 15 | - name: The CELO Blog 16 | url: https://medium.com/celoorg 17 | about: Learn more about what's new and coming to CELO. 18 | - name: CELO on Reddit 19 | url: https://www.reddit.com/r/celo/ 20 | about: Check out The Official CELO Subreddit. 21 | -------------------------------------------------------------------------------- /scripts/push.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ##### 4 | # Sends a push notification to a specific device 5 | # 6 | ##### 7 | 8 | 9 | # celo-mobile-alfajores 10 | # https://console.firebase.google.com/project/celo-mobile-alfajores/settings/cloudmessaging/ios:org.celo.mobile.alfajores 11 | FCM_SERVER_KEY="" 12 | # to get your token you can add a log in src/firebase/firebase.ts registerTokenToDb 13 | USER_FCM_TOKEN="" 14 | 15 | curl -X POST --header "Authorization: key=$FCM_SERVER_KEY" \ 16 | --Header "Content-Type: application/json" \ 17 | https://fcm.googleapis.com/fcm/send \ 18 | -d '{ 19 | "notification": { 20 | "title": "Test Title", 21 | "body": "Tap this to be redirected to celo.org" 22 | }, 23 | "android": { 24 | "ttl": 604800000, 25 | "priority": "normal", 26 | "notification": { 27 | "icon": "ic_stat_rings", 28 | "color": "#42D689" 29 | } 30 | }, 31 | "data": { 32 | "ou": "https://celo.org" 33 | }, 34 | "to": "'"$USER_FCM_TOKEN"'" 35 | } ' 36 | -------------------------------------------------------------------------------- /.github/workflows/e2e-ios.yml: -------------------------------------------------------------------------------- 1 | name: E2E - iOS 2 | on: 3 | workflow_call: 4 | inputs: 5 | ios-version: 6 | required: true 7 | type: string 8 | 9 | jobs: 10 | ios: 11 | name: iOS (${{ inputs.ios-version }}) 12 | # We use custom runners to speed up the build 13 | # Replace this with `macos-14` to run on standard GitHub Actions runners 14 | # runs-on: macos-14 15 | runs-on: ios-e2e-group 16 | timeout-minutes: 45 17 | steps: 18 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 19 | - run: brew install fastlane cocoapods 20 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 21 | id: setup-node 22 | with: 23 | node-version-file: 'package.json' 24 | check-latest: true 25 | - uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # 8.2.1 26 | with: 27 | eas-version: latest 28 | token: ${{ secrets.EXPO_TOKEN }} 29 | - run: yarn install 30 | - run: eas build --platform ios --profile e2e --local 31 | -------------------------------------------------------------------------------- /plugins/withCustomGradleProperties.js: -------------------------------------------------------------------------------- 1 | // Modified from https://github.com/mersiades/expo-router-branch-mre/blob/f56aae045e9a2f106bdd5f12176d6f98fda959a0/plugins/withCustomGradleProperties.js 2 | // TODO: publish as an official expo config plugin 3 | const { withGradleProperties } = require('@expo/config-plugins') 4 | 5 | /** 6 | * Use this plugin to customize the gradle.properties file produced by Expo 7 | * @param {ExpoConfig} config 8 | * @param {Object.} [properties] An object where each key-value pair represents a Gradle property 9 | * @return {ExpoConfig} 10 | */ 11 | module.exports = (config, properties = {}) => { 12 | return withGradleProperties(config, (config) => { 13 | Object.entries(properties).forEach(([key, value]) => { 14 | const property = { type: 'property', key, value } 15 | const index = config.modResults.findIndex((item) => item.key === key) 16 | 17 | if (index >= 0) { 18 | config.modResults[index] = property 19 | } else { 20 | config.modResults.push(property) 21 | } 22 | }) 23 | 24 | return config 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /.github/actions/yarn-install/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Yarn install' 2 | description: 'Runs setup node + cache + yarn install' 3 | runs: 4 | using: 'composite' 5 | steps: 6 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 7 | id: setup-node 8 | with: 9 | node-version-file: 'package.json' 10 | check-latest: true 11 | - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 12 | with: 13 | path: | 14 | node_modules 15 | */*/node_modules 16 | key: ${{ runner.os }}-${{ runner.arch }}-node-${{ steps.setup-node.outputs.node-version }}-${{ hashFiles('**/yarn.lock', 'patches/*.patch') }} 17 | - name: Install Yarn dependencies 18 | run: | 19 | # Deals with yarn (v1) install flakiness 20 | yarn || yarn --network-concurrency 1 21 | # Another install flakiness fix (at least in yarn v1 with node 20) 22 | # yarn postinstall isn't always run (?!) so we run it manually 23 | yarn postinstall 24 | shell: bash 25 | - name: Fail if someone forgot to commit "yarn.lock" 26 | run: git diff --exit-code 27 | shell: bash 28 | -------------------------------------------------------------------------------- /.github/scripts/createAppVersionPr.ts: -------------------------------------------------------------------------------- 1 | import { config, echo, env, exec, ShellString } from 'shelljs' 2 | 3 | config.fatal = true 4 | 5 | const branchName = new ShellString(env.BRANCH_NAME ?? '') 6 | const VALORA_BOT_TOKEN = new ShellString(env.VALORA_BOT_TOKEN ?? '') 7 | 8 | // ensure that we are using ssh 9 | exec('git remote set-url origin git@github.com:valora-inc/wallet.git') 10 | 11 | echo('Create version bump branch from main') 12 | exec(`git checkout -b ${branchName}`) 13 | 14 | echo('Bump app version') 15 | exec('yarn pre-deploy --minor') 16 | 17 | const appVersion = exec('node -p "require(\'./package.json\').version"') 18 | 19 | echo('Push changes to branch') 20 | exec('git add .') 21 | exec('git commit -m "Bump app version to $app_version"') 22 | exec(`git push --set-upstream origin ${branchName}`) 23 | 24 | echo('Open version bump PR') 25 | exec( 26 | ` 27 | curl -u "valora-bot:${VALORA_BOT_TOKEN}" \ 28 | -X POST \ 29 | -H "Accept: application/vnd.github.v3+json" \ 30 | https://api.github.com/repos/valora-inc/wallet/pulls \ 31 | -d '${JSON.stringify({ 32 | head: branchName, 33 | base: 'main', 34 | title: `chore: bump app version to ${appVersion}`, 35 | })}' 36 | `, 37 | ) 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🐛 Bug report' 3 | about: Report a bug found while using Valora. 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | ### Current behavior 16 | 17 | 18 | 19 | ### Desired behavior 20 | 21 | 22 | 23 | ### Steps to Reproduce 24 | 25 | 26 | 27 | ### Versions 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /knip.ts: -------------------------------------------------------------------------------- 1 | import type { KnipConfig } from 'knip' 2 | 3 | const config: KnipConfig = { 4 | entry: [ 5 | 'index.tsx!', 6 | 'app.config.js!', 7 | 'metro.config.js!', 8 | '.github/scripts/*.ts', 9 | './scripts/**/*.{js,ts}', 10 | ], 11 | ignoreDependencies: [ 12 | '@actions/github', 13 | 'babel-preset-expo', // not listed in package.json so we use the version used by expo 14 | // required by expo 15 | 'expo-build-properties', 16 | 'expo-dev-client', 17 | 'expo-font', 18 | 'expo-splash-screen', 19 | 'expo-status-bar', 20 | // peer deps for @valora/eslint-config-typescript 21 | '@typescript-eslint/eslint-plugin', 22 | 'eslint-plugin-import', 23 | 'eslint-plugin-jest', 24 | 'eslint-plugin-react', 25 | 'eslint-plugin-react-hooks', 26 | 'eslint-plugin-react-native', 27 | // patches https://www.npmjs.com/package/patch-package#why-use-postinstall-postinstall-with-yarn 28 | 'postinstall-postinstall', 29 | ], 30 | ignoreBinaries: [ 31 | 'eas', // Expo Application Services 32 | 'licenses', // Yarn command 33 | 'scripts/generate-release-notes.ts', // Used by release-nightly workflow 34 | ], 35 | ignore: [ 36 | '.github/scripts/autoApprovePr.js', // Used by bump-app-version workflow 37 | '.github/scripts/enableAutomergeOnPr.js', // Used by bump-app-version workflow 38 | ], 39 | } 40 | 41 | export default config 42 | -------------------------------------------------------------------------------- /plugins/withDesugaring.js: -------------------------------------------------------------------------------- 1 | // https://github.com/expo/expo/discussions/31071#discussioncomment-11128404 2 | // Required for @google-cloud/recaptcha-enterprise-react-native - Android build fails with "Dependency requires core library desugaring to be enabled" 3 | const { withAppBuildGradle } = require('expo/config-plugins') 4 | 5 | module.exports = function withCustomAppBuildGradle(config) { 6 | return withAppBuildGradle(config, async (config) => { 7 | const androidPattern = '\nandroid {\n' 8 | const androidIndex = config.modResults.contents.indexOf(androidPattern) 9 | const androidPivot = androidIndex + androidPattern.length + 1 10 | config.modResults.contents = 11 | config.modResults.contents.slice(0, androidPivot) + 12 | ' compileOptions {\n coreLibraryDesugaringEnabled true\n }\n\n ' + 13 | config.modResults.contents.slice(androidPivot) 14 | 15 | const dependenciesPattern = '\ndependencies {\n' 16 | const dependenciesIndex = 17 | config.modResults.contents.indexOf(dependenciesPattern) 18 | const dependenciesPivot = dependenciesIndex + dependenciesPattern.length + 1 19 | config.modResults.contents = 20 | config.modResults.contents.slice(0, dependenciesPivot) + 21 | ' coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")\n\n ' + 22 | config.modResults.contents.slice(dependenciesPivot) 23 | 24 | return config 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": false 4 | }, 5 | "files.exclude": { 6 | "**/*.js": { "when": "$(basename).ts" }, 7 | "**/*.js.map": true 8 | }, 9 | "files.watcherExclude": { 10 | "**/.git/objects/**": true, 11 | "**/.git/subtree-cache/**": true, 12 | "**/node_modules/*/**": true 13 | }, 14 | "typescript.preferences.importModuleSpecifier": "non-relative", 15 | "typescript.updateImportsOnFileMove.enabled": "always", 16 | // Use the project's TypeScript version instead of the one bundled with VS Code 17 | "typescript.tsdk": "node_modules/typescript/lib", 18 | "editor.codeActionsOnSave": { 19 | "source.organizeImports": "never" 20 | }, 21 | "[javascript]": { 22 | "editor.formatOnSave": true, 23 | "editor.codeActionsOnSave": { 24 | "source.organizeImports": "never" 25 | } 26 | }, 27 | "[javascriptreact]": { 28 | "editor.formatOnSave": true, 29 | "editor.codeActionsOnSave": { 30 | "source.organizeImports": "explicit" 31 | } 32 | }, 33 | "[typescript]": { 34 | "editor.formatOnSave": true, 35 | "editor.codeActionsOnSave": { 36 | "source.organizeImports": "explicit" 37 | } 38 | }, 39 | "[typescriptreact]": { 40 | "editor.formatOnSave": true, 41 | "editor.codeActionsOnSave": { 42 | "source.organizeImports": "explicit" 43 | } 44 | }, 45 | "javascript.format.enable": false, 46 | "editor.tabSize": 2, 47 | "editor.detectIndentation": false 48 | } 49 | -------------------------------------------------------------------------------- /plugins/withAndroidAppThemeFullScreen.js: -------------------------------------------------------------------------------- 1 | const { 2 | withAndroidStyles, 3 | createRunOncePlugin, 4 | WarningAggregator, 5 | } = require('@expo/config-plugins') 6 | 7 | const TAG = 'withAppThemeFullScreen' 8 | 9 | const withAndroidAppThemeFullScreen = (config) => { 10 | return withAndroidStyles(config, (config) => { 11 | try { 12 | const styles = config.modResults 13 | 14 | // Find existing AppTheme 15 | const appTheme = styles.resources.style?.find( 16 | (style) => style.$?.name === 'AppTheme', 17 | ) 18 | 19 | if (!appTheme) { 20 | WarningAggregator.addWarningAndroid( 21 | TAG, 22 | 'AppTheme not found in styles.xml. The windowFullscreen setting is not added. The splash screen may not display correctly.', 23 | ) 24 | return config 25 | } 26 | 27 | // Add windowFullscreen if not already present 28 | if ( 29 | !appTheme.item?.some( 30 | (item) => item.$?.name === 'android:windowFullscreen', 31 | ) 32 | ) { 33 | appTheme.item = [ 34 | ...(appTheme.item ?? []), 35 | { $: { name: 'android:windowFullscreen' }, _: 'true' }, 36 | ] 37 | } 38 | 39 | return config 40 | } catch (e) { 41 | WarningAggregator.addWarningAndroid( 42 | TAG, 43 | `Failed to update AppTheme in styles.xml: ${e.message}`, 44 | ) 45 | return config 46 | } 47 | }) 48 | } 49 | 50 | module.exports = createRunOncePlugin(withAndroidAppThemeFullScreen, TAG) 51 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "cli": { 3 | "version": ">= 5.9.1", 4 | "promptToConfigurePushNotifications": false, 5 | "appVersionSource": "remote" 6 | }, 7 | "build": { 8 | "e2e": { 9 | "withoutCredentials": true, 10 | "android": { 11 | "buildType": "apk" 12 | }, 13 | "ios": { 14 | "simulator": true 15 | }, 16 | "env": { 17 | "EXPO_PUBLIC_DIVVI_E2E": "true" 18 | } 19 | }, 20 | "base": { 21 | "autoIncrement": true, 22 | "distribution": "store", 23 | "node": "24.11.1", 24 | "env": { 25 | "EXPO_NO_TELEMETRY": "1" 26 | } 27 | }, 28 | "mainnet": { 29 | "extends": "base", 30 | "environment": "production", 31 | "env": { 32 | "APP_VARIANT": "mainnet" 33 | } 34 | }, 35 | "mainnet-nightly": { 36 | "extends": "base", 37 | "environment": "preview", 38 | "env": { 39 | "APP_VARIANT": "mainnet-nightly" 40 | } 41 | } 42 | }, 43 | "submit": { 44 | "mainnet": { 45 | "android": { 46 | "track": "internal", 47 | "releaseStatus": "completed" 48 | }, 49 | "ios": { 50 | "ascAppId": "1520414263", 51 | "appleTeamId": "HDPUB8C3KG" 52 | } 53 | }, 54 | "mainnet-nightly": { 55 | "android": { 56 | "track": "internal", 57 | "releaseStatus": "completed" 58 | }, 59 | "ios": { 60 | "ascAppId": "1599290566", 61 | "appleTeamId": "HDPUB8C3KG" 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/e2e-main.yml: -------------------------------------------------------------------------------- 1 | name: E2E - Main 2 | on: 3 | push: 4 | branches: 5 | - main 6 | workflow_dispatch: 7 | inputs: 8 | logLevel: 9 | description: 'Log level' 10 | required: true 11 | default: 'warning' 12 | tags: 13 | description: 'End-to-end Tests' 14 | # Cron job to run e2e tests @ 8:30 pm daily on the latest commit on the default branch - main 15 | schedule: 16 | - cron: '30 20 * * *' 17 | 18 | # Cancel any in progress run of the workflow for a given PR 19 | # This avoids building outdated code 20 | concurrency: 21 | group: ${{ github.workflow }}-${{ github.ref_name }} 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | android: 26 | name: Android 27 | strategy: 28 | max-parallel: 2 29 | fail-fast: false 30 | matrix: 31 | # min supported API level is 24 32 | # 24 is failing due to Let's Encrypt root certificate expiration 33 | # 25 is failing (RET-1274) 34 | # 26 is failing (RET-1275) 35 | android-api-level: [27] 36 | uses: ./.github/workflows/e2e-android.yml 37 | with: 38 | android-api-level: ${{ matrix.android-api-level }} 39 | secrets: inherit 40 | ios: 41 | name: iOS 42 | strategy: 43 | max-parallel: 2 44 | fail-fast: false 45 | matrix: 46 | # 15.0 is not included as it runs on the merge queue 47 | ios-version: ['17.2'] 48 | uses: ./.github/workflows/e2e-ios.yml 49 | with: 50 | ios-version: ${{ matrix.ios-version }} 51 | secrets: inherit 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Note: consider updating .easignore with any changes made to this file 3 | ################################################################################ 4 | 5 | ios/ 6 | android/ 7 | 8 | # Expo 9 | # 10 | .expo/ 11 | dist/ 12 | web-build/ 13 | expo-env.d.ts 14 | build-* 15 | 16 | # Python 17 | # 18 | *.pyc 19 | 20 | # Vim 21 | # 22 | *.swo 23 | *.swp 24 | *.swm 25 | *.swn 26 | 27 | # OSX 28 | # 29 | .DS_Store 30 | 31 | # node.js 32 | # 33 | node_modules/ 34 | npm-debug.log 35 | yarn-error.log 36 | 37 | */**/junit.xml 38 | 39 | coverage 40 | coverage.json 41 | 42 | lerna-debug.log 43 | 44 | # keys 45 | .env.mnemonic* 46 | !.env.mnemonic*.enc 47 | *.jks 48 | *.p8 49 | *.p12 50 | *.key 51 | *.pem 52 | secrets.json 53 | 54 | .terraform/ 55 | tsconfig.tsbuildinfo 56 | 57 | # git mergetool 58 | *.orig 59 | 60 | */.next/* 61 | .env 62 | .env.local 63 | 64 | # Ruby gem artifacts 65 | .bundle 66 | vendor/ 67 | 68 | # Bundle artifact 69 | *.jsbundle 70 | 71 | fastlane/google-play-service-account.json 72 | 73 | test-report.xml 74 | coverage/ 75 | 76 | android/keystore.properties 77 | android/app/celo-release-key.keystore 78 | android/app/.settings 79 | android/.settings 80 | 81 | .vscode/ 82 | 83 | # End to end tests 84 | e2e/artifacts/ 85 | e2e_run.log 86 | e2e_pidcat_run.log 87 | e2e/test-results/* 88 | !e2e/test-results/teststyle.css 89 | !e2e/test-results/valora-icon.png 90 | 91 | # Temporary files created by Metro to check the health of the file watcher 92 | .metro-health-check* 93 | -------------------------------------------------------------------------------- /assets/Logo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { StyleSheet, View, ViewStyle } from 'react-native' 3 | import Svg, { Defs, G, LinearGradient, Path, Stop } from 'react-native-svg' 4 | 5 | interface Props { 6 | size?: number 7 | color?: string 8 | style?: ViewStyle 9 | testID?: string 10 | } 11 | 12 | export default function Logo({ 13 | style, 14 | size = 32, 15 | color = 'url(#prefix__paint0_linear)', 16 | testID, 17 | }: Props) { 18 | return ( 19 | 20 | 21 | 22 | 26 | 27 | 28 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | const styles = StyleSheet.create({ 46 | container: { 47 | shadowOffset: { width: 0, height: 0 }, 48 | shadowRadius: 2, 49 | shadowOpacity: 1, 50 | shadowColor: 'rgba(46, 51, 56, 0.15)', 51 | }, 52 | }) 53 | -------------------------------------------------------------------------------- /RELEASES.md: -------------------------------------------------------------------------------- 1 | # Release Automation 🤖 2 | 3 | This repository contains the configuration that automate nightly and release builds for Valora `mainnet` and `alfajores`. 4 | 5 | ## Automated Build Types 6 | 7 | ### Nightly Builds 8 | 9 | Nightly builds are run on weekdays daily at 03:00 UTC using GitHub Action's schedule / CRON job feature. This CRON job will setup the environment, compile Valora Nightly for `mainnet` and `alfajores`, and upload them to Google Play and Apple TestFlight. Build status, completion or failure, is then reported in the #eng-release-notifications channel in Slack. 10 | 11 | ### Release / Production Builds 12 | 13 | Release builds are manually triggered using GitHub's `workflow_dispatch` feature. The `workflow_dispatch` takes in an input of "Release Branch Name" which should be the branch that the release manager has created and updated the release version from 1.19.0 to 1.20.0 etc. When triggered, it will setup the environment, compile Valora for `mainnet` and `alfajores` and upload it to Google Play and Apple TestFlight. Build status, completion or failure, is then reported in the #eng-release-notifications channel in Slack. 14 | 15 | ## How does it know which commit to build? 16 | 17 | Release / production builds use the specific branch / commit hash that is provided to the workflow using the `workflow_dispatch` input. The nightly builds on the otherhand use the latest commit on the wallet repo's main branch at the time of the run. 18 | 19 | ## Why the unix time for version build codes? 20 | 21 | Using the unix time from the Git commit allows us to have a specific commit associated to each build we push and it's easy to lookup the commit from that value if needed: 22 | 23 | ``` 24 | git log --format='%H %ct' | grep TIMESTAMP 25 | ``` 26 | -------------------------------------------------------------------------------- /.github/scripts/enableAutomergeOnPr.js: -------------------------------------------------------------------------------- 1 | // This script uses JSDoc annotations to get TypeScript checks 2 | // Why aren't we using TypeScript directly here? 3 | // `actions/github-script` doesn't support TypeScript scripts directly :/ 4 | 5 | /** 6 | * Note: we can remove typeof below once we upgrade TypeScript 7 | * otherwise we get a crash, see https://github.com/microsoft/TypeScript/issues/36830 8 | * @typedef {ReturnType} GitHub 9 | * @typedef {import('@actions/github').context} Context 10 | */ 11 | 12 | const enableAutomergeQuery = `mutation ($pullRequestId: ID!, $mergeMethod: PullRequestMergeMethod!) { 13 | enablePullRequestAutoMerge(input: { 14 | pullRequestId: $pullRequestId, 15 | mergeMethod: $mergeMethod 16 | }) { 17 | pullRequest { 18 | autoMergeRequest { 19 | enabledAt 20 | } 21 | } 22 | } 23 | }` 24 | 25 | /** 26 | * @param {Object} obj - An object. 27 | * @param {GitHub} obj.github 28 | * @param {Context} obj.context 29 | */ 30 | module.exports = async ({ github, context }) => { 31 | const { owner, repo } = context.repo 32 | const { BRANCH_NAME } = process.env 33 | 34 | console.log(`Looking for PR with branch name ${BRANCH_NAME}`) 35 | const listPrs = await github.rest.pulls.list({ 36 | owner, 37 | repo, 38 | state: 'open', 39 | head: `${context.repo.owner}:${BRANCH_NAME}`, 40 | }) 41 | const pr = listPrs.data[0] 42 | 43 | if (!pr) { 44 | console.log(`No PR with branch name ${BRANCH_NAME} found`) 45 | return 46 | } 47 | 48 | console.log(`Enabling automerge on PR #${pr.number}`) 49 | await github.graphql(enableAutomergeQuery, { 50 | pullRequestId: pr.node_id, 51 | mergeMethod: 'SQUASH', 52 | }) 53 | 54 | console.log('Done') 55 | } 56 | -------------------------------------------------------------------------------- /docs/connecting-dapps.md: -------------------------------------------------------------------------------- 1 | # Connecting Dapps 2 | 3 | Valora supports [WalletConnect v2](https://docs.walletconnect.com/2.0/) for connecting Dapps to Valora. [WalletConnect v1 is end-of-life](https://docs.walletconnect.com/2.0/advanced/migration-from-v1.x/overview) and not supported. 4 | 5 | If you're building a Dapp we recommend [rainbowkit](https://github.com/rainbow-me/rainbowkit) or [Web3Modal](https://github.com/WalletConnect/web3modal) to make it easy to connect your Dapp to Valora via WalletConnect. Take a look at a [complete example](https://docs.celo.org/developer/rainbowkit-celo) to help you get started. 6 | 7 | ## WalletConnect details 8 | 9 | Supported actions: https://github.com/celo-org/wallet/blob/main/src/walletConnect/constants.ts#L3 10 | 11 | Docs for WalletConnect v2: https://docs.walletconnect.com/2.0 12 | 13 | ## Troubleshooting tips 14 | 15 | When building the connection between your Dapp and Valora, it can be challenging to determine the source of a connection error. We recommend using the official WalletConnect example Dapp and wallet to help with this. 16 | 17 | - If Valora cannot connect to your Dapp but is able to connect to the [WalletConnect v2 example react Dapp](https://react-app.walletconnect.com/) correctly, the issue likely lies with your Dapp. It can be helpful to check the [implementation details](https://github.com/WalletConnect/web-examples/tree/main/dapps/react-dapp-v2) of this example Dapp against your own implementation. 18 | - If your Dapp is unable to connect to the [WalletConnect v2 example wallet](https://react-wallet.walletconnect.com/), there is likely an issue with your Dapp. As above, we recommend comparing the implementation details between your Dapp and the example Dapp provided. 19 | 20 | If these troubleshooting steps don't help, please join our [`#dapp-dev` channel on Discord](https://discord.gg/gQvjYv5Fqh) to discuss your specific problem. 21 | -------------------------------------------------------------------------------- /docs/deeplinks.md: -------------------------------------------------------------------------------- 1 | # Deeplinks 2 | 3 | ## Payment 4 | 5 | You can create a deeplink that will prompt the user to make a payment to an address. 6 | 7 | The deeplink will look like this `celo://wallet/pay?{...queryParams}` 8 | where the query parameters can be: 9 | 10 | - `address` (required): The address that will be the recipient of the payment. 11 | - `displayName` (optional): The URL-encoded name of the recipient. If you leave this empty the address will be shown instead. 12 | - `comment` (optional): A URL-encoded text that explains the reason for the payment. 13 | - `token` (optional): The token you want the payment to be in. Can be `cUSD`, `cEUR` or `CELO`. If you pass a different value or nothing the app will use cUSD. 14 | - `amount` (optional): The amount to send. If you don't pass a value the user will have to input the amount if `token` is `cUSD` or not work at all if `token` is `CELO`. 15 | - `currencyCode` (optional, recommended if amount is set): The fiat currency in which the user will see the payment amount. Users of the app can choose which currency they see values in the app by default so if you set the amount it's strongly recommended to set a value for this so you make sure you receive the expected amount. Possible values are the ones listed [in the `LocalCurrencyCode` enum here](https://github.com/celo-org/wallet/blob/main/src/localCurrency/consts.ts#L2) 16 | 17 | To URL encode a text you can use the [encodeURI](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI) function or a web tool like https://www.urlencoder.org/ 18 | 19 | Example payment deeplink: 20 | 21 | `celo://wallet/pay?address=0x4b371df8d05abd2954564b54faf10b8c8f1bc3a2&displayName=Example%20name&amount=9.50&comment=Burger%20with%20fries&token=cUSD¤cyCode=USD` 22 | 23 | Smallest possible payment deeplink: 24 | 25 | `celo://wallet/pay?address=0x4b371df8d05abd2954564b54faf10b8c8f1bc3a2` 26 | -------------------------------------------------------------------------------- /docs/watching-assets.mdx: -------------------------------------------------------------------------------- 1 | # Registering Tokens with Users 2 | 3 | Valora includes a default list of popular tokens. Each token in the 4 | default list must meet the asset listing requirements. 5 | However, there are many tokens that users 6 | want to manage and transact with in Valora that do not meet the requirements. To 7 | support these tokens well, Valora implements a deeplink handler so 8 | that a dapp can construct deeplinks that make it easy for their users 9 | to add arbitrary tokens to Valora. 10 | 11 | ## Using deeplinks 12 | 13 | > This feature is in-development and not fully supported yet! 14 | 15 | Valora's deeplink handler requires the contract address. If your 16 | [ERC-20](https://eips.ethereum.org/EIPS/eip-20) contract implements 17 | `symbol` and `decimals` Valora will fetch the token symbol and 18 | decimals from your contract. If your ERC-20 contract does not implement 19 | `symbol` or `decimals`, you must specify them in the deeplink URL. 20 | 21 | ```ts 22 | // Required: The address that the token is at 23 | const address = '0xb92463e2262e700e29c16416270c9fdfa17934d7' 24 | // Required if not implemented in contract: a ticker symbol or shorthand, up to 5 chars. 25 | const symbol = 'C4P' 26 | // Required if not implemented in contract: the number of decimals in the token 27 | const decimals = 6 28 | // Optional: a URL to download token image (must be a 256 x 256 PNG) 29 | const image = 'https://image-hosting.foo/256x256.png' 30 | 31 | const watchAssetDeeplink = `valora://wallet/watchAsset?address=${address}&symbol=${symbol}&decimals=${decimals}&image=${encodeURIComponent( 32 | image, 33 | )}` 34 | ``` 35 | 36 | ## Upcoming: using EIP-747 37 | 38 | > This feature is planned and will be supported soon! 39 | 40 | Valora aims to support 41 | [EIP-747](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-747.md) 42 | to make it easy for dapp integrated with MetaMask's watch token 43 | functionality to also support Valora. 44 | -------------------------------------------------------------------------------- /.easignore: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Note: this file is an exact copy of .gitignore with exclusions for eas/.env 3 | # to facilitate local builds. They are listed in the bottom part of the file. 4 | ################################################################################ 5 | 6 | ios/ 7 | android/ 8 | 9 | # Expo 10 | # 11 | .expo/ 12 | dist/ 13 | web-build/ 14 | expo-env.d.ts 15 | build-* 16 | 17 | # Python 18 | # 19 | *.pyc 20 | 21 | # Vim 22 | # 23 | *.swo 24 | *.swp 25 | *.swm 26 | *.swn 27 | 28 | # OSX 29 | # 30 | .DS_Store 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | yarn-error.log 37 | 38 | */**/junit.xml 39 | 40 | coverage 41 | coverage.json 42 | 43 | lerna-debug.log 44 | 45 | # keys 46 | .env.mnemonic* 47 | !.env.mnemonic*.enc 48 | *.jks 49 | *.p8 50 | *.p12 51 | *.key 52 | *.pem 53 | secrets.json 54 | 55 | .terraform/ 56 | tsconfig.tsbuildinfo 57 | 58 | # git mergetool 59 | *.orig 60 | 61 | */.next/* 62 | .env 63 | .env.local 64 | 65 | # Ruby gem artifacts 66 | .bundle 67 | vendor/ 68 | 69 | # Bundle artifact 70 | *.jsbundle 71 | 72 | fastlane/google-play-service-account.json 73 | 74 | test-report.xml 75 | coverage/ 76 | 77 | android/keystore.properties 78 | android/app/celo-release-key.keystore 79 | android/app/.settings 80 | android/.settings 81 | 82 | .vscode/ 83 | 84 | # End to end tests 85 | e2e/artifacts/ 86 | e2e_run.log 87 | e2e_pidcat_run.log 88 | e2e/test-results/* 89 | !e2e/test-results/teststyle.css 90 | !e2e/test-results/valora-icon.png 91 | 92 | # Temporary files created by Metro to check the health of the file watcher 93 | .metro-health-check* 94 | 95 | ################################################################################ 96 | # Here's what's different from .gitignore 97 | ################################################################################ 98 | 99 | # Exclude ./eas/.env contents to facilitate the local builds 100 | !.eas/ 101 | !.eas/.env 102 | !.eas/.env/** 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Valora Mobile App 2 | 3 | ![Checks](https://github.com/valora-inc/wallet/actions/workflows/check.yml/badge.svg) 4 | ![E2E](https://github.com/valora-inc/wallet/actions/workflows/e2e-main.yml/badge.svg) 5 | [![Codecov](https://img.shields.io/codecov/c/github/valora-inc/wallet)](https://codecov.io/gh/valora-inc/wallet) 6 | [![GitHub contributors](https://img.shields.io/github/contributors/valora-inc/wallet)](https://github.com/valora-inc/wallet/graphs/contributors) 7 | [![GitHub commit activity](https://img.shields.io/github/commit-activity/w/valora-inc/wallet)](https://github.com/valora-inc/wallet/graphs/contributors) 8 | [![GitHub Stars](https://img.shields.io/github/stars/valora-inc/wallet.svg)](https://github.com/valora-inc/wallet/stargazers) 9 | ![GitHub repo size](https://img.shields.io/github/repo-size/valora-inc/wallet) 10 | [![GitHub](https://img.shields.io/github/license/valora-inc/wallet?color=blue)](https://github.com/valora-inc/wallet/blob/master/LICENSE) 11 | 12 | Valora is a mobile wallet focused on making global peer-to-peer 13 | payments simple and accessible to anyone. It supports the Celo 14 | Identity Protocol which allows users to verify their phone number and 15 | send payments to their contacts. 16 | 17 | ## Integrate with Valora 18 | 19 | Connect your Dapp to Valora using [WalletConnect](./docs/connecting-dapps.md). 20 | 21 | See also [deeplinks.md](./docs/deeplinks.md) for integrating with Valora using deep links. 22 | 23 | ## Build Valora Locally 24 | 25 | To setup Valora locally, follow [setup instructions](./WALLET.md). 26 | 27 | ## Repo Structure 28 | 29 | The repository follows the default React Native Android and iOS app structure. 30 | 31 | Code owners can be found in [.github/CODEOWNERS](.github/CODEOWNERS). 32 | 33 | ## Releases 34 | 35 | This repository is configured to build and upload the nightly releases automatically. The production release process has to be started manually by running a dedicated workflow. See [RELEASES.md](./RELEASES.md) to learn more. 36 | 37 | ## Contributing 38 | 39 | We welcome contributions in the form of Issues and PRs. Please read [CONTRIBUTING.md](CONTRIBUTING.md) first! 40 | 41 | ## Community 42 | 43 | Have questions or need help? Join our [Discord Community](https://discord.com/invite/J5XMtMkwC4). 44 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | extends: ['github>valora-inc/renovate-config:default.json5'], 3 | 4 | // Restrict semantic commit type to "chore" 5 | // See: https://docs.renovatebot.com/presets-default/#semanticprefixfixdepschoreothers 6 | // https://github.com/renovatebot/renovate/discussions/14026 7 | ignorePresets: [':semanticPrefixFixDepsChoreOthers'], 8 | 9 | // Limit number of concurrent renovate branches/PRs, to avoid spamming the repo 10 | prConcurrentLimit: 4, 11 | 12 | // The order of objects in the packageRules array does matter, 13 | // in the sense that rules declared later (towards the end of the array) 14 | // overwrite values of an also-matching rule declared earlier. 15 | packageRules: [ 16 | { 17 | // Set higher priority for node dependencies 18 | matchManagers: ['npm'], 19 | prPriority: 2, 20 | }, 21 | { 22 | // Allow renovate to update node 23 | matchDepTypes: ['engines'], 24 | enabled: true, 25 | // a bit higher priority for node updates 26 | prPriority: 3, 27 | }, 28 | { 29 | // Disable dependencies updates 30 | // As they need to be compatible with the @divvi/mobile peerDependencies 31 | matchDepTypes: ['dependencies'], 32 | // Exclude @divvi/* packages from this group 33 | matchPackageNames: ['!@divvi/*'], 34 | enabled: false, 35 | }, 36 | { 37 | // For now follow the alpha tag for @divvi/mobile 38 | // We'll remove this once we're ready to move to a stable release 39 | matchPackageNames: ['@divvi/mobile'], 40 | followTag: 'alpha', 41 | schedule: ['at any time'], 42 | }, 43 | { 44 | // Disable updates for expo-splash-screen to support the full-screen splash on Android 45 | // See https://github.com/valora-inc/wallet/pull/6556 46 | matchPackageNames: ['expo-splash-screen'], 47 | enabled: false, 48 | }, 49 | { 50 | // Group devDependencies updates 51 | matchDepTypes: ['devDependencies'], 52 | groupName: 'devDependencies', 53 | // But exclude some specific packages from this group 54 | matchPackageNames: ['!typescript', '!prettier'], 55 | // Set default priority for dev dependencies 56 | prPriority: 0, 57 | }, 58 | ], 59 | } 60 | -------------------------------------------------------------------------------- /assets/WelcomeLogo.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { ViewStyle } from 'react-native' 3 | import Svg, { Path } from 'react-native-svg' 4 | 5 | interface Props { 6 | width?: number 7 | height?: number 8 | style?: ViewStyle 9 | } 10 | 11 | export default function WelcomeLogo({ 12 | width = 243, 13 | height = 64, 14 | style, 15 | }: Props) { 16 | return ( 17 | 24 | 28 | 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/bump-app-version.yml: -------------------------------------------------------------------------------- 1 | name: Bump App Version 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | # trigger on new minor release tags only 7 | tags: 8 | - valora-v[0-9]+.[0-9]+.0 9 | 10 | jobs: 11 | bump-app-version: 12 | # this job uses the pre-deploy script and agvtool which is only available on macos 13 | runs-on: macos-15 14 | permissions: 15 | pull-requests: write 16 | env: 17 | BRANCH_NAME: valora-bot/bump-app-version 18 | steps: 19 | - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 20 | with: 21 | project_id: celo-mobile-mainnet 22 | credentials_json: ${{ secrets.MAINNET_SERVICE_ACCOUNT_KEY }} 23 | - name: Google Secrets 24 | id: google-secrets 25 | uses: google-github-actions/get-secretmanager-secrets@bc9c54b29fdffb8a47776820a7d26e77b379d262 # v3.0.0 26 | with: 27 | secrets: |- 28 | BOT_SSH_KEY:projects/1027349420744/secrets/BOT_SSH_PRIVATE_KEY 29 | VALORA_BOT_TOKEN:projects/1027349420744/secrets/VALORA_BOT_TOKEN 30 | - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 31 | with: 32 | ssh-private-key: ${{ steps.google-secrets.outputs.BOT_SSH_KEY }} 33 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 34 | with: 35 | ref: main 36 | - uses: ./.github/actions/yarn-install 37 | - run: yarn ts-node .github/scripts/createAppVersionPr.ts 38 | env: 39 | VALORA_BOT_TOKEN: ${{ steps.google-secrets.outputs.VALORA_BOT_TOKEN }} 40 | - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 41 | with: 42 | script: | 43 | const allowedUpdatedFiles = [ 44 | 'package.json' 45 | ] 46 | const script = require('.github/scripts/autoApprovePr.js') 47 | await script({github, context, core, allowedUpdatedFiles}) 48 | - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 49 | with: 50 | github-token: ${{ steps.google-secrets.outputs.VALORA_BOT_TOKEN }} 51 | script: | 52 | const script = require('.github/scripts/enableAutomergeOnPr.js') 53 | await script({github, context, core}) 54 | -------------------------------------------------------------------------------- /scripts/sync-eas-node-version.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | // This script syncs the Node.js version from package.json engines to eas.json 3 | 4 | import fs from 'fs' 5 | import path from 'path' 6 | import packageJson from '../package.json' 7 | 8 | interface EasConfig { 9 | build?: { 10 | base?: { 11 | node?: string 12 | } 13 | } 14 | } 15 | 16 | /** 17 | * Sync Node.js version from package.json engines to eas.json 18 | */ 19 | function syncEasNodeVersion(): void { 20 | const easJsonPath = path.join(__dirname, '..', 'eas.json') 21 | 22 | try { 23 | // Get Node.js version from package.json 24 | const nodeVersion = packageJson.engines?.node 25 | 26 | if (!nodeVersion) { 27 | console.error('❌ No Node.js version found in package.json engines field') 28 | process.exit(1) 29 | } 30 | 31 | console.log(`📦 Found Node.js version in package.json: ${nodeVersion}`) 32 | 33 | // Read eas.json 34 | const easConfig: EasConfig = JSON.parse( 35 | fs.readFileSync(easJsonPath, 'utf8'), 36 | ) 37 | const currentEasNodeVersion = easConfig.build?.base?.node 38 | 39 | if (!currentEasNodeVersion) { 40 | console.error( 41 | '❌ No Node.js version found in eas.json build.base.node field', 42 | ) 43 | process.exit(1) 44 | } 45 | 46 | console.log( 47 | `🏗️ Current Node.js version in eas.json: ${currentEasNodeVersion}`, 48 | ) 49 | 50 | // Check if versions match 51 | if (nodeVersion === currentEasNodeVersion) { 52 | console.log('✅ Node.js versions are already in sync!') 53 | return 54 | } 55 | 56 | // Update eas.json 57 | if (!easConfig.build) { 58 | easConfig.build = {} 59 | } 60 | if (!easConfig.build.base) { 61 | easConfig.build.base = {} 62 | } 63 | easConfig.build.base.node = nodeVersion 64 | 65 | // Write back to eas.json with proper formatting 66 | fs.writeFileSync(easJsonPath, JSON.stringify(easConfig, null, 2) + '\n') 67 | 68 | console.log( 69 | `🔄 Updated eas.json Node.js version: ${currentEasNodeVersion} → ${nodeVersion}`, 70 | ) 71 | console.log('✅ Node.js versions are now in sync!') 72 | } catch (error) { 73 | console.error( 74 | '❌ Error syncing Node.js versions:', 75 | (error as Error).message, 76 | ) 77 | process.exit(1) 78 | } 79 | } 80 | 81 | syncEasNodeVersion() 82 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | import { createApp } from '@divvi/mobile' 2 | import { registerRootComponent } from 'expo' 3 | import Constants from 'expo-constants' 4 | import Logo from './assets/Logo' 5 | import WelcomeLogo from './assets/WelcomeLogo' 6 | import background from './assets/background.jpg' 7 | import introBackground from './assets/intro-background.png' 8 | 9 | const expoConfig = Constants.expoConfig 10 | if (!expoConfig) { 11 | throw new Error('expoConfig is not available') 12 | } 13 | 14 | const App = createApp({ 15 | registryName: expoConfig.extra?.registryName, 16 | displayName: expoConfig.name, 17 | deepLinkUrlScheme: expoConfig.scheme![0], 18 | ios: { appStoreId: expoConfig.extra?.appStoreId }, 19 | networks: expoConfig.extra?.networks, 20 | divviProtocol: { 21 | divviId: '0x9eCfE3dDFAf1BB9B55f56b84471406893c5E29ad', 22 | }, 23 | features: { 24 | cloudBackup: true, 25 | segment: { apiKey: process.env.EXPO_PUBLIC_SEGMENT_API_KEY! }, 26 | sentry: { clientUrl: process.env.EXPO_PUBLIC_SENTRY_CLIENT_URL! }, 27 | statsig: { apiKey: process.env.EXPO_PUBLIC_STATSIG_API_KEY! }, 28 | walletConnect: { 29 | projectId: process.env.EXPO_PUBLIC_WALLET_CONNECT_PROJECT_ID!, 30 | }, 31 | }, 32 | themes: { 33 | default: { 34 | assets: { 35 | brandLogo: Logo, 36 | welcomeLogo: WelcomeLogo, 37 | welcomeBackgroundImage: introBackground, 38 | onboardingSuccessBackgroundImage: background, 39 | splashBackgroundImage: background, 40 | }, 41 | }, 42 | }, 43 | locales: expoConfig.locales ?? {}, 44 | screens: { 45 | tabs: ({ defaultTabs }) => { 46 | return { 47 | screens: [ 48 | defaultTabs.wallet, 49 | defaultTabs.activity, 50 | defaultTabs.discover, 51 | ], 52 | initialScreen: 'activity', 53 | } 54 | }, 55 | }, 56 | experimental: { 57 | firebase: expoConfig.extra?.firebaseEnabled ?? false, 58 | alchemyApiKey: process.env.EXPO_PUBLIC_ALCHEMY_API_KEY!, 59 | bidali: { 60 | url: process.env.EXPO_PUBLIC_BIDALI_URL!, 61 | }, 62 | inviteFriends: { 63 | shareUrl: 'www.valora.xyz', 64 | }, 65 | notificationCenter: true, 66 | phoneNumberVerification: true, 67 | zendeskConfig: { 68 | apiKey: process.env.EXPO_PUBLIC_ZENDESK_API_KEY!, 69 | projectName: 'valoraapp', 70 | }, 71 | otaTranslations: { 72 | crowdinDistributionHash: 'e-f9f6869461793b9d1a353b2v7c', 73 | }, 74 | recaptcha: { 75 | iosSiteKey: '6LfSJ9grAAAAAGutFwc2FJdW6U2x8-x62AtOmbc9', 76 | androidSiteKey: '6LfB_tYrAAAAAHYwSI9MPdXJE29PbtgRYnLb66aZ', 77 | }, 78 | }, 79 | }) 80 | 81 | // registerRootComponent calls AppRegistry.registerComponent('main', () => App); 82 | // It also ensures that whether you load the app in Expo Go or in a native build, 83 | // the environment is set up appropriately 84 | registerRootComponent(App) 85 | -------------------------------------------------------------------------------- /.github/workflows/e2e-android.yml: -------------------------------------------------------------------------------- 1 | name: E2E - Android 2 | on: 3 | workflow_call: 4 | inputs: 5 | android-api-level: 6 | required: true 7 | type: number 8 | 9 | jobs: 10 | android: 11 | name: Android (SDK ${{ inputs.android-api-level }}) 12 | # We use custom runners to speed up the build 13 | # Replace this with `ubuntu-latest` to run on standard GitHub Actions runners 14 | runs-on: 15 | - nscloud-ubuntu-22.04-amd64-16x64-with-cache 16 | - nscloud-cache-size-20gb 17 | - nscloud-cache-tag-wallet-e2e-android 18 | timeout-minutes: 25 19 | steps: 20 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 21 | - name: Set env 22 | run: | 23 | ANDROID_HOME="$HOME/android-tools" 24 | echo "ANDROID_HOME=$ANDROID_HOME" >> $GITHUB_ENV 25 | echo "$ANDROID_HOME/cmdline-tools/latest/bin" >> $GITHUB_PATH 26 | echo "$ANDROID_HOME/platform-tools" >> $GITHUB_PATH 27 | echo "$ANDROID_HOME/emulator" >> $GITHUB_PATH 28 | echo "ANDROID_SDK_ROOT=" >> $GITHUB_ENV 29 | # See https://namespace.so/docs/actions/nscloud-cache-action 30 | - name: Cache 31 | uses: namespacelabs/nscloud-cache-action@v1 32 | with: 33 | cache: gradle 34 | path: ${{ env.ANDROID_HOME}} 35 | - uses: actions/setup-java@f2beeb24e141e01a676f977032f5a29d81c9e27e # v5.1.0 36 | with: 37 | distribution: 'temurin' 38 | java-version: '17' 39 | - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 40 | id: setup-node 41 | with: 42 | node-version-file: 'package.json' 43 | check-latest: true 44 | - name: Setup runner 45 | run: | 46 | set -x 47 | 48 | mkdir -p $ANDROID_HOME 49 | 50 | curl https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip -o cli-tools.zip 51 | unzip cli-tools.zip -d "$ANDROID_HOME/cmdline-tools" 52 | # Command line tools need to be placed in $ANDROID_HOME/cmdline-tools/latest to function properly 53 | # and because we cache $ANDROID_HOME, we need remove the existing version and move the new one 54 | rm -rf "$ANDROID_HOME/cmdline-tools/latest" 55 | mv "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/latest" 56 | 57 | # Temporarily disable checking for EPIPE error and use yes to accept all licenses 58 | set +o pipefail 59 | yes | sdkmanager --licenses 60 | set -o pipefail 61 | 62 | # Install Ninja 63 | sudo apt-get update -y 64 | sudo apt-get install ninja-build 65 | shell: bash 66 | - uses: expo/expo-github-action@c7b66a9c327a43a8fa7c0158e7f30d6040d2481e # 8.2.1 67 | with: 68 | eas-version: latest 69 | token: ${{ secrets.EXPO_TOKEN }} 70 | - run: yarn install 71 | - run: eas build --platform android --profile e2e --local 72 | -------------------------------------------------------------------------------- /.github/scripts/autoApprovePr.js: -------------------------------------------------------------------------------- 1 | // This script uses JSDoc annotations to get TypeScript checks 2 | // Why aren't we using TypeScript directly here? 3 | // `actions/github-script` doesn't support TypeScript scripts directly :/ 4 | 5 | /** 6 | * Note: we can remove typeof below once we upgrade TypeScript 7 | * otherwise we get a crash, see https://github.com/microsoft/TypeScript/issues/36830 8 | * @typedef {ReturnType} GitHub 9 | * @typedef {import('@actions/github').context} Context 10 | */ 11 | 12 | /** 13 | * @param {Object} obj - An object. 14 | * @param {GitHub} obj.github 15 | * @param {Context} obj.context 16 | * @param {Array} obj.allowedUpdatedFiles 17 | */ 18 | module.exports = async ({ github, context, allowedUpdatedFiles }) => { 19 | const { owner, repo } = context.repo 20 | const { BRANCH_NAME } = process.env 21 | 22 | console.log(`Looking for PR with branch name ${BRANCH_NAME}`) 23 | const listPrs = await github.rest.pulls.list({ 24 | owner, 25 | repo, 26 | state: 'open', 27 | head: `${context.repo.owner}:${BRANCH_NAME}`, 28 | }) 29 | const pr = listPrs.data[0] 30 | 31 | if (!pr) { 32 | console.log(`No PR with branch name ${BRANCH_NAME} found`) 33 | return 34 | } 35 | 36 | if (allowedUpdatedFiles.length > 0) { 37 | console.log( 38 | `Verifying that expected files are modified for PR #${pr.number}`, 39 | ) 40 | const listFiles = await github.rest.pulls.listFiles({ 41 | owner, 42 | repo, 43 | pull_number: pr.number, 44 | }) 45 | 46 | const missingFiles = allowedUpdatedFiles.filter( 47 | (file) => !listFiles.data.find(({ filename }) => filename === file), 48 | ) 49 | if (missingFiles.length > 0) { 50 | console.log( 51 | `Files updated in PR #${pr.number} do not match the expectation`, 52 | ) 53 | console.log( 54 | `The following files were not updated: ${missingFiles.join(', ')}`, 55 | ) 56 | return 57 | } 58 | 59 | const extraFiles = listFiles.data.filter( 60 | ({ filename }) => !allowedUpdatedFiles.includes(filename), 61 | ) 62 | if (extraFiles.length > 0) { 63 | console.log( 64 | `Files updated in PR #${pr.number} do not match the expectation`, 65 | ) 66 | console.log( 67 | `The following modified files were not expected: ${extraFiles 68 | .map(({ filename }) => filename) 69 | .join(', ')}`, 70 | ) 71 | return 72 | } 73 | } else { 74 | console.log( 75 | `Skipping verification for expected modified files because none were provided`, 76 | ) 77 | } 78 | 79 | console.log(`Approving PR #${pr.number}`) 80 | await github.rest.pulls.createReview({ 81 | owner, 82 | repo, 83 | pull_number: pr.number, 84 | event: 'APPROVE', 85 | body: `Approved from [${context.workflow} #${context.runNumber}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}).`, 86 | }) 87 | 88 | console.log('Done') 89 | } 90 | -------------------------------------------------------------------------------- /.github/scripts/uploadE2eBuildToEmerge.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as $ from 'shelljs' 3 | 4 | $.config.fatal = true 5 | 6 | // Adapted from https://github.com/EmergeTools/emerge-upload-action/blob/d2f4fc1627edd5b8c346d75d77565da83a97f443/src/inputs.ts 7 | function getGitHubInfo() { 8 | // On PRs, the GITHUB_SHA refers to the merge commit instead 9 | // of the commit that triggered this action. 10 | // Therefore, on a PR we need to explicitly get the head sha from the event json data. 11 | let sha 12 | let baseSha 13 | let branchName 14 | let prNumber 15 | const eventFile = fs.readFileSync(process.env.GITHUB_EVENT_PATH ?? '', { 16 | encoding: 'utf8', 17 | }) 18 | const eventFileJson = JSON.parse(eventFile) 19 | if (process.env.GITHUB_EVENT_NAME === 'pull_request') { 20 | sha = eventFileJson?.pull_request?.head?.sha ?? process.env.GITHUB_SHA ?? '' 21 | baseSha = eventFileJson?.pull_request?.base?.sha ?? '' 22 | branchName = process.env.GITHUB_HEAD_REF ?? '' 23 | prNumber = eventFileJson?.number ?? '' 24 | 25 | if (!prNumber) { 26 | throw new Error('Could not get prNumber for a PR triggered build.') 27 | } 28 | } else if (process.env.GITHUB_EVENT_NAME === 'push') { 29 | sha = process.env.GITHUB_SHA ?? '' 30 | // Get the SHA of the previous commit, which will be the baseSha in the case of a push event. 31 | baseSha = eventFileJson?.before ?? '' 32 | branchName = process.env.GITHUB_REF_NAME ?? '' 33 | } else if (process.env.GITHUB_EVENT_NAME === 'merge_group') { 34 | sha = process.env.GITHUB_SHA ?? '' 35 | // Get the SHA of the base commit, which will be the base_sha in the case of a merge_group event. 36 | baseSha = eventFileJson?.merge_group?.base_sha ?? '' 37 | branchName = process.env.GITHUB_REF_NAME ?? '' 38 | } else { 39 | throw new Error( 40 | `Unsupported action trigger: ${process.env.GITHUB_EVENT_NAME}`, 41 | ) 42 | } 43 | 44 | if (!sha) { 45 | throw new Error('Could not get SHA of the head branch.') 46 | } 47 | if (!baseSha) { 48 | throw new Error('Could not get SHA of the base branch.') 49 | } 50 | if (!branchName) { 51 | throw new Error('Could not get name of the branch.') 52 | } 53 | 54 | const repoName = process.env.GITHUB_REPOSITORY ?? '' 55 | if (repoName === '') { 56 | throw new Error('Could not get repository name.') 57 | } 58 | 59 | return { 60 | sha, 61 | baseSha, 62 | repoName, 63 | prNumber, 64 | branchName, 65 | } 66 | } 67 | 68 | const { sha, baseSha, repoName, prNumber, branchName } = getGitHubInfo() 69 | 70 | const packageJson = JSON.parse( 71 | fs.readFileSync('package.json', { encoding: 'utf8' }), 72 | ) 73 | const filePath = 74 | packageJson.detox.apps[process.env.DETOX_CONFIG ?? ''].binaryPath 75 | 76 | // Note: we're not using the Emerge GitHub Action directly because it doesn't support packaging .app into a xcarchive 77 | // The fastlane action does support it though 78 | // Also this requires the EMERGE_API_TOKEN env var to be set 79 | $.exec(`bundle exec fastlane run emerge \ 80 | file_path:${filePath} \ 81 | repo_name:${repoName} \ 82 | sha:${sha} \ 83 | base_sha:${baseSha} \ 84 | branch:${branchName} \ 85 | pr_number:${prNumber || ''} \ 86 | tag:e2e 87 | `) 88 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Valora is open source and we welcome open participation! 4 | 5 | ## Contributing PRs 6 | 7 | Find an area that is of interest and you would like to help with. Look 8 | for issues that are tagged as "[good first 9 | issue](https://github.com/valora-inc/wallet/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)" 10 | or "[help 11 | wanted](https://github.com/valora-inc/wallet/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)" 12 | to get started. If you’d like to dig deeper, feel free to look at 13 | other labels and TODO’s in code comments. If there’s an issue you’re 14 | interested in contributing to or taking over, assign yourself to it 15 | and add a comment with your plans to address and target timeline. If 16 | there’s already someone assigned to it, please check with them before 17 | adding yourself to the assignee list. 18 | 19 | Tasks range from minor to major improvements. Based on your interests, 20 | skillset, and level of comfort with the code-base feel free to 21 | contribute where you see appropriate. We encourage you to PR \(pull 22 | request\) your work regularly and often to solicit feedback and to 23 | ensure everyone has an idea of what you’re working on. If you’ve just 24 | started, we suggest creating a PR with “WIP” \(Work In Progress\) in 25 | the title and let us know when it’s ready to review in the comments. 26 | 27 | Valora is part of the Celo community and follows Celo's [Code of 28 | Conduct](https://celo.org/code-of-conduct). 29 | 30 | ### Contributing _substantial_ changes 31 | 32 | A substantial change is one that significantly alters the user 33 | experience or an underlying implementation. For example, we would 34 | consider the following as examples of substantial changes: 35 | 36 | - Adding support for in-app swaps. 37 | - Changing how Valora calculates asset value in local fiat. 38 | - Changing Valora's branding (_e.g.,_ color palette). 39 | - Adding a way for Valora users to go through decentralized KYC. 40 | - Removing screens from onboarding. 41 | 42 | If you're considering making what might be a substantial change, we 43 | strongly encourage you to reach out to the Valora engineering team 44 | first. We might have suggestions that guide the implementation and 45 | make it easier to implement a high-quality change to Valora with the 46 | least amount of work. `#valora-dev` on Valora's [Discord 47 | server](./README.md#community) is a great way to contact us. 48 | 49 | ### Please make sure your PR 50 | 51 | - Requests the appropriate reviewers. When in doubt, consult the 52 | CODEOWNERS file for suggestions. 53 | - Provides a comprehensive description of the problem addressed and 54 | changes made. 55 | - Explains dependencies and backwards incompatible changes. 56 | - Contains unit and end-to-end tests and a description of how these 57 | were run. 58 | - Includes changes to relevant documentation. 59 | 60 | ## Contributing Issues 61 | 62 | If you are submitting an issue, please double check that there doesn’t 63 | already exist an issue for the work you have in mind. 64 | 65 | - Has a clear detailed title such that it can’t be confused with other Valora issues. 66 | - Provides a comprehensive description of the current and expected 67 | behavior including, if relevant, links to external references and 68 | specific implementation guidelines. 69 | - Is tagged with the relevant labels. 70 | - Is assigned if you or someone else is already working on it. 71 | -------------------------------------------------------------------------------- /.github/workflows/release-nightly.yml: -------------------------------------------------------------------------------- 1 | name: Release - Nightly 2 | 3 | on: 4 | workflow_dispatch: 5 | # At 03:00 (UTC) daily 6 | # https://crontab.guru/#0_3_*_*_* 7 | # This is a good time for nightly builds, across CET, PST and IST (QA team in India) 8 | schedule: 9 | - cron: '0 3 * * *' 10 | 11 | jobs: 12 | check-date: 13 | runs-on: ubuntu-latest 14 | name: Check latest commit 15 | outputs: 16 | should-run: ${{ steps.should-run.outputs.should-run }} 17 | latest-commit-hash: ${{ steps.latest-commit-hash.outputs.latest-commit-hash }} 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 21 | - id: latest-commit-hash 22 | name: Print latest commit 23 | run: echo $(git rev-parse HEAD) && echo "latest-commit-hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT 24 | - id: should-run 25 | continue-on-error: true 26 | name: Check latest commit is less than a day 27 | if: ${{ github.event_name == 'schedule' }} 28 | run: test -z $(git rev-list --after="24 hours" $(git rev-parse HEAD)) && echo "should-run=false" >> $GITHUB_OUTPUT 29 | 30 | trigger-expo-eas-nightly: 31 | runs-on: ubuntu-latest 32 | needs: check-date 33 | if: ${{ needs.check-date.outputs.should-run != 'false' }} 34 | name: Trigger Expo EAS nightly build 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 38 | with: 39 | # Fetch all history for all tags and branches 40 | fetch-depth: 0 41 | - name: Authenticate to Google Cloud 42 | uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 43 | with: 44 | project_id: celo-mobile-mainnet 45 | credentials_json: ${{ secrets.GCP_MAINNET_RELEASE_AUTOMATION_SERVICE_ACCOUNT_KEY }} 46 | - name: Google Secrets 47 | id: google-secrets 48 | uses: google-github-actions/get-secretmanager-secrets@bc9c54b29fdffb8a47776820a7d26e77b379d262 # v3.0.0 49 | with: 50 | secrets: |- 51 | SLACK_WEBHOOK_URL:projects/1027349420744/secrets/SLACK_WEBHOOK_URL 52 | BOT_SSH_KEY:projects/1027349420744/secrets/BOT_SSH_PRIVATE_KEY 53 | - uses: webfactory/ssh-agent@a6f90b1f127823b31d4d4a8d96047790581349bd # v0.9.1 54 | with: 55 | ssh-private-key: ${{ steps.google-secrets.outputs.BOT_SSH_KEY }} 56 | - uses: ./.github/actions/yarn-install 57 | - name: Generate release notes 58 | run: | 59 | RELEASE_NOTES=$(yarn --silent ts-node scripts/generate-release-notes.ts --lastTag tags/nightly --toRef ${{ needs.check-date.outputs.latest-commit-hash }} --verbose false) 60 | echo -e "RELEASE_NOTES<> $GITHUB_ENV 61 | # This will trigger the EAS build 62 | - name: Update nightly tag 63 | run: | 64 | git tag -f nightly "${{ needs.check-date.outputs.latest-commit-hash }}" 65 | git push origin --force tags/nightly 66 | - name: Build notification 67 | uses: edge/simple-slack-notify@d841831738af1d83ecc27186e722322145c21488 # v1.1.2 68 | with: 69 | status: success 70 | success_text: '🚀 Nightly EAS build has been triggered and should be available shortly! 🥳' 71 | # the RELEASE_NOTES are generated in the previous step 72 | fields: | 73 | [ 74 | { 75 | "title": "Changes included", 76 | "value": ${JSON.stringify(env.RELEASE_NOTES)} 77 | } 78 | ] 79 | env: 80 | SLACK_WEBHOOK_URL: ${{ steps.google-secrets.outputs.SLACK_WEBHOOK_URL }} 81 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | name: Check 2 | 3 | on: 4 | # ..any pull request, workflow dispatch and merge queue (covers main) 5 | pull_request: 6 | workflow_dispatch: 7 | merge_group: 8 | # Cron job to run checks @ 8:30 pm daily on the latest commit on the default branch - main 9 | schedule: 10 | - cron: '30 20 * * *' 11 | 12 | # Cancel any in progress run of the workflow for a given PR 13 | # This avoids building outdated code 14 | concurrency: 15 | # Fallback used github.ref_name as it is always defined 16 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | vulnerability: 21 | name: Vulnerabilities 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 25 | - uses: ./.github/actions/yarn-install 26 | - run: ./scripts/ci_check_vulnerabilities.sh 27 | 28 | lint: 29 | name: Lint 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 33 | - uses: ./.github/actions/yarn-install 34 | - run: yarn run format:check 35 | - run: yarn run lint 36 | - run: yarn typecheck 37 | - run: yarn tsc -p .github/scripts 38 | yarn-lock: 39 | name: 'yarn.lock Up-to-date' 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 43 | - uses: ./.github/actions/yarn-install 44 | - run: git diff --exit-code 45 | licenses: 46 | name: Licenses 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 50 | - uses: ./.github/actions/yarn-install 51 | - run: yarn check-licenses 52 | knip: 53 | name: Knip 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 57 | - uses: ./.github/actions/yarn-install 58 | - run: yarn knip 59 | # Ensure the release notes script is working 60 | release-notes: 61 | name: Release Notes 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 65 | with: 66 | # Fetch all history for all tags and branches 67 | fetch-depth: 0 68 | - uses: ./.github/actions/yarn-install 69 | - run: yarn generate-release-notes 70 | # Sync Node.js version between package.json and eas.json 71 | eas-node-sync: 72 | name: EAS Node.js Version Sync 73 | runs-on: ubuntu-latest 74 | steps: 75 | - uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 76 | with: 77 | project_id: celo-mobile-mainnet 78 | credentials_json: ${{ secrets.MAINNET_SERVICE_ACCOUNT_KEY }} 79 | - name: Google Secrets 80 | id: google-secrets 81 | uses: google-github-actions/get-secretmanager-secrets@bc9c54b29fdffb8a47776820a7d26e77b379d262 # v3.0.0 82 | with: 83 | secrets: |- 84 | VALORA_BOT_TOKEN:celo-mobile-mainnet/VALORA_BOT_TOKEN 85 | - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 86 | with: 87 | token: ${{ steps.google-secrets.outputs.VALORA_BOT_TOKEN }} 88 | # See https://github.com/actions/checkout?tab=readme-ov-file#push-a-commit-to-a-pr-using-the-built-in-token 89 | ref: ${{ github.head_ref || github.ref }} 90 | - uses: ./.github/actions/yarn-install 91 | - run: yarn sync-eas-node-version 92 | - name: Check for EAS node version changes 93 | id: eas-node-version 94 | run: | 95 | if [ -n "$(git status --porcelain eas.json)" ]; then 96 | echo "changes=true" >> $GITHUB_OUTPUT 97 | else 98 | echo "changes=false" >> $GITHUB_OUTPUT 99 | fi 100 | - name: Commit EAS node version 101 | if: "${{ steps.eas-node-version.outputs.changes == 'true' && github.event_name == 'pull_request' && !startsWith(github.event.head_commit.message, 'chore: auto sync eas node version') }}" 102 | run: | 103 | git config user.name "Divvi Bot" 104 | git config user.email "89419329+divvi-bot@users.noreply.github.com" 105 | git add eas.json 106 | git commit -m "chore: auto sync eas node version" 107 | git push 108 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Security Announcements 4 | 5 | Public announcements of new releases with security fixes and of disclosure of any vulnerabilities will be made in the Celo Forum's [Security Announcements](https://forum.celo.org/c/security-announcements/) channel. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | We’re extremely grateful for security researchers and users that report vulnerabilities to the Celo community. All reports are thoroughly investigated. 10 | 11 | **Please do not file a public ticket** mentioning any vulnerability. 12 | 13 | The Celo community asks that all suspected vulnerabilities be privately and responsibly disclosed. 14 | 15 | To make a report, submit your vulnerability to [Celo on HackerOne](https://hackerone.com/celo). 16 | 17 | You can also email the [security@celo.org](mailto:security@celo.org) list with the details of reproducing the vulnerability as well as the usual details expected for all bug reports. 18 | 19 | While the primary focus of this disclosure program is the Celo protocol and the Celo wallet, the team may be able to assist in coordinating a response to a vulnerability in the third-party apps or tools in the Celo ecosystem. 20 | 21 | You may encrypt your email to this list using this GPG key (but encryption using GPG is NOT required to make a disclosure): 22 | 23 | ``` 24 | -----BEGIN PGP PUBLIC KEY BLOCK----- 25 | 26 | mQINBF5Vg0MBEADNmoPEf2HiSGGqJZOE8liv783KZVKRle5oTwI9VNF7rnHUq0ub 27 | /jf6B/saUliO8JbYTyfbUXfPjaeIRxA1zvbHPMtWdj6coPUFwvZ77okDHeXGAnFl 28 | 6ZcKs/q8mpcNP8E4ATtvrNUW3aRkDvud2e+ysIHyjaae1mf29cWMGInxjm3YUyMr 29 | 5/YnJEzSiVN+krtTDDVg4N2qZbR1gX7uvVlXytzD92vKWNurWi2ZXhwWhC0BbcCK 30 | HlHnEhok2njMqmKlY1rzj35hNwzxwj8fZi3JGgTPQAUZP6vHqvo7GxmUYPQqo/f0 31 | 2y7dshL3An7AM3OMIWbtwsh72PX+SqeKTF9Y00TsYz4raVv4ub2HT/0TtOwBlNJD 32 | fBr3XgRMkUtBGhaGWe8D4P6UNUM/imz8EQLCbFa6qhYr+asrYzvCGHHNy9v3OeJF 33 | fyYyqn/k+44zMTCZx7FGR/SFjEDnROqjFmOYio+Tuv5U7ycJu8Bhu2qqCCxYApR6 34 | NyVZQ1U7cTWTLLWkFfe359pSM2KhK9ftuRm2Jf1CmkgTsxYxpzyuFYf37FvI8LFK 35 | 06h1R5yr7lTNY0EfKG6UWTHmJo5oYSTJ1tWEItxT0H4cjCRVyJxs2ze9sdPc1a6f 36 | hP05fk+VLyN29WgmVuKtrMHUjtWwVhjbmOe9fCMHe4+N/4jPZ/ivvT5c2QARAQAB 37 | tDJDZWxvIFZ1bG5lcmFiaWxpdHkgRGlzY2xvc3VyZXMgPHNlY3VyaXR5QGNlbG8u 38 | b3JnPokCTgQTAQgAOBYhBPXFzhvm4XlmxS2jdaoCJpJIGPpJBQJeVYNDAhsDBQsJ 39 | CAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJEKoCJpJIGPpJqqcQAKWp35OEhP/Je8Xg 40 | XmMuBeaOqdjdWoBQD7lYIBwd6I+Y1vF6mk69yWLhWqIqgSaTf8BtHyZmuKPAs8rX 41 | mSwdQY9X/cy3ECLBFP5TABGwZx6F6cYNXH6b/drSo6Mh/ZEJE/M4rwuHImEIVO9f 42 | jljA66HaNOZp3C9CBVqUlsJkezXR7JCWcKwrlzXyG7NeP/6/yqlCADX8zgGEjBLa 43 | bsFrSLnBgxncSmxEmSjlT3BxpUv6T5QGu/QxfhmgqgmwzUKA5Ak1aZ4jxZQ0W5ZB 44 | bT2jyifJXszg1qeZ+bbyFSINqiuDB9/gJTNwZ5WTtf2Q8HBJOS6i0uRpHaXqY841 45 | 4srFeu7bOM35FCUL6kZ2snMQSVuw9cnvWtcby/rJZ3QUFn0Sffh4b/c7zS+/aUOM 46 | 8e2MrF76veHa4uqA1gHOtsT2LKtyaVn6HdImeMH0TQJVvAm8u8B0jrcgB1z8BjrR 47 | qbJeJnqjz9pGQo1AMuuSJ5pj3BemfGlUyDx4KuJVJBFDqzcP8DjOEYY9ylcqS7MO 48 | lZS36e00npd2sHcty0FmHpYKMODNfZ+XW6OAfOMX9eZlispOV5JmT78FTr5HLHjG 49 | oTOBGeOTDB4Y16d4EO47AZvOBPMuXsw2SJ4aqyyxhMVlgdnp+0A3CMpkv0BnvVFT 50 | ewaQUMIrvaTKyF5B4Mf3E0pTDQ5OuQINBF5Vg0MBEACzQa+flDsevx9Utb5hQN+S 51 | mKPQpkno7AN1PdLA23DAiXoV2L3Mwa7lpqSKrwo8yRnlq7aNo6j4/G1xnmgFyd6c 52 | oYHWOwSfF/nkio2Stjf6UQSe8WlHNwKTOyXA2ABZ9Xr4BpeuTh+tZIf02VEfdDEd 53 | tTfCcq/iA/UScUDG2LmPcFt62shjJ5+bP0HHalIn28kgaTJIWgp8tWkIFvt7TRYt 54 | UrBxcjgmVnpb2eC1AI9UoQ04+7hV4mXb19GfM0WdssmOPArtmHo2daGg/WwcbgJ8 55 | oqSBe/DEqmjrn7ETNn1wbCgsA8nPS7NCrBxl0pFEiav+2ZJ0B6jOA1m8UI4FQUPS 56 | oXnZMyq7jd8liBeWQrrK8OpUUKcyaBDnM31wNgmWJAq9Ck42JnnLijYoESbpOk9r 57 | e0RnDv2H86oIBXpwnVRZebJgbw2Nuv9GsvCB4hYsBbL0UVrxRGl7pOnRnEMd94RY 58 | P4KzB6lELVxUDt3NCs/PFXSKId9NpLYxomox6B+9SDh9e0MnFTn8vT8hOEkCBYMW 59 | bZldMAengb7jagC0Z2TfwukiDWMcVHObhxjR6fl03ACnt+EXqAZjr5x2QOko7CXU 60 | xBo+wTDZTmuD6S8LI0q9v0BrIszZiJxWti7BSqYGnDPOLA84d0p8DIEy6W9GSqAD 61 | CBfYi5r24BkLaslh6Uyb2QARAQABiQI2BBgBCAAgFiEE9cXOG+bheWbFLaN1qgIm 62 | kkgY+kkFAl5Vg0MCGwwACgkQqgImkkgY+kkhXw//YtV8tNpmDo2oZfUltYE0sZrr 63 | YN/0wchkHMs3rUt6K8/5Lbbzi3GcLVtG5PSbkGs6eTfbCYzJkhQO+vdA+T5CgZld 64 | HrEQ4nPXHBcr7BFBRPQ4LCM7q4ygVldRw2qusWvf/YdcMmLk7pg/F1wSSkWZHUpp 65 | a0BNbZBeCZ1xWlu/+VUlCpsT2m5Ak1gdm58zwJ4uZwTc4hRPxS7q4GuSQBNrvNx8 66 | Os6Grt6lcZyJ6zGYr73/5PraZyQnprQ4FzJjwSLb7doVfhUoGVf4wiECsVhoNByu 67 | /ojfERTErUzhw9Wu44EmY0Imot99SbbJ9ifchF69TnSMcb17PNnbV10VbbQuNLun 68 | 7GiTfF3wd80MhI+KpDAlFpy08M5i+kyk97uzMzFcOZn68KUbNIsn/JFaKc1orMmy 69 | b8mhFCXTeX1UJsfSwRodSmKRrKSPq6MflNQYPwpKOU5hOHaQ71gavNunQVEbEoXO 70 | ny+/RyALrlDt0ffuKoVBHHQjThIGXnb1ItTjuCI5d6yiuFuRz3AvHmhqm/4sqX96 71 | t1yroLhsk8x7HaLuVdKB/SW+DhTAxPqFJzEw09KpZci5m9VMyJxplI3uh0rJb6pz 72 | HssYE9a+LjVUEzu3sZxvKhDzG7v4Nn7NZ4Ve8Q5tRZJEXvwEi6KKBxrDKV9wKdqY 73 | ztmP6RvHPG6jYvyqofo= 74 | =Sy1W 75 | -----END PGP PUBLIC KEY BLOCK----- 76 | ``` 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@valora/wallet", 3 | "version": "1.121.0", 4 | "author": "Valora Inc", 5 | "license": "Apache-2.0", 6 | "private": true, 7 | "main": "index.tsx", 8 | "engines": { 9 | "node": "24.11.1" 10 | }, 11 | "scripts": { 12 | "prebuild": "expo prebuild --clean", 13 | "start": "expo start", 14 | "android": "expo run:android", 15 | "ios": "expo run:ios", 16 | "env:mainnet": "APP_VARIANT=mainnet eas env:pull production --non-interactive", 17 | "env:mainnet-dev": "APP_VARIANT=mainnet-dev eas env:pull development --non-interactive", 18 | "env:mainnet-nightly": "APP_VARIANT=mainnet-nightly eas env:pull preview --non-interactive", 19 | "typecheck": "tsc --noEmit", 20 | "lint": "eslint --ext=.tsx,.ts .", 21 | "format": "prettier --loglevel error --write .", 22 | "format:check": "prettier --check .", 23 | "knip": "knip", 24 | "reset": "yarn reset-modules && yarn reset-yarn", 25 | "reset-modules": "rm -rf node_modules/", 26 | "reset-yarn": "yarn cache clean", 27 | "check-licenses": "yarn licenses list --prod | grep '\\(─ GPL\\|─ (GPL-[1-9]\\.[0-9]\\+ OR GPL-[1-9]\\.[0-9]\\+)\\)' && echo 'Found GPL license(s). Use 'yarn licenses list --prod' to look up the offending package' && exit 1 || echo 'No GPL licenses found'", 28 | "sync-eas-node-version": "ts-node ./scripts/sync-eas-node-version.ts", 29 | "pre-deploy": "yarn version --no-git-tag-version", 30 | "generate-release-notes": "ts-node ./scripts/generate-release-notes.ts", 31 | "postinstall": "patch-package" 32 | }, 33 | "dependencies": { 34 | "@divvi/cookies": "^6.2.3", 35 | "@divvi/mobile": "^1.0.0-alpha.121", 36 | "@divvi/react-native-fs": "^2.20.1", 37 | "@divvi/react-native-keychain": "^10.0.0-divvi.1", 38 | "@google-cloud/recaptcha-enterprise-react-native": "^18.8.0", 39 | "@interaxyz/react-native-webview": "^13.13.4", 40 | "@react-native-async-storage/async-storage": "^2.2.0", 41 | "@react-native-clipboard/clipboard": "^1.16.3", 42 | "@react-native-community/netinfo": "^11.4.1", 43 | "@react-native-firebase/analytics": "22.4.0", 44 | "@react-native-firebase/app": "22.4.0", 45 | "@react-native-firebase/auth": "22.4.0", 46 | "@react-native-firebase/database": "22.4.0", 47 | "@react-native-firebase/messaging": "22.4.0", 48 | "@react-native-masked-view/masked-view": "^0.3.2", 49 | "@react-native-picker/picker": "^2.11.2", 50 | "@react-navigation/bottom-tabs": "^7.2.1", 51 | "@react-navigation/devtools": "^7.0.16", 52 | "@react-navigation/elements": "^2.2.6", 53 | "@react-navigation/material-top-tabs": "^7.1.1", 54 | "@react-navigation/native": "^7.0.15", 55 | "@react-navigation/native-stack": "^7.2.1", 56 | "@segment/analytics-react-native": "^2.21.3", 57 | "@segment/analytics-react-native-plugin-adjust": "^0.8.0", 58 | "@segment/analytics-react-native-plugin-destination-filters": "^1.1.0", 59 | "@segment/analytics-react-native-plugin-firebase": "^0.4.3", 60 | "@segment/sovran-react-native": "^1.1.3", 61 | "@sentry/react-native": "^6.20.0", 62 | "@statsig/react-native-bindings": "^3.17.2", 63 | "@walletconnect/react-native-compat": "^2.19.0", 64 | "expo": "^53.0.22", 65 | "expo-build-properties": "~0.14.8", 66 | "expo-camera": "~16.1.11", 67 | "expo-constants": "~17.1.7", 68 | "expo-dev-client": "~5.2.4", 69 | "expo-font": "~13.3.2", 70 | "expo-splash-screen": "~0.27.7", 71 | "expo-status-bar": "~2.2.3", 72 | "lottie-react-native": "^5.1.6", 73 | "react": "19.0.0", 74 | "react-native": "0.79.6", 75 | "react-native-adjust": "^4.38.1", 76 | "react-native-app-control": "^1.0.2", 77 | "react-native-auth0": "5.0.0-beta.5", 78 | "react-native-config": "^1.5.5", 79 | "react-native-contacts": "https://github.com/valora-inc/react-native-contacts#9940121", 80 | "react-native-device-info": "^13.2.0", 81 | "react-native-gesture-handler": "^2.23.1", 82 | "react-native-haptic-feedback": "^2.3.3", 83 | "react-native-in-app-review": "^4.3.3", 84 | "react-native-launch-arguments": "^4.0.2", 85 | "react-native-linear-gradient": "^2.8.3", 86 | "react-native-localize": "^3.3.0", 87 | "react-native-pager-view": "^6.7.1", 88 | "react-native-permissions": "^4.1.5", 89 | "react-native-quick-crypto": "0.7.7", 90 | "react-native-reanimated": "~3.17.4", 91 | "react-native-restart": "^0.0.27", 92 | "react-native-safe-area-context": "^5.6.1", 93 | "react-native-screens": "~4.11.1", 94 | "react-native-shake": "5.5.2", 95 | "react-native-share": "^11.1.0", 96 | "react-native-simple-toast": "^3.3.2", 97 | "react-native-svg": "^15.11.2", 98 | "react-native-video": "^6.16.1" 99 | }, 100 | "devDependencies": { 101 | "@actions/github": "^5.1.1", 102 | "@babel/core": "^7.26.10", 103 | "@expo/config-plugins": "~10.1.2", 104 | "@types/react": "~19.0.10", 105 | "@types/shelljs": "^0.8.15", 106 | "@types/yargs": "^17.0.33", 107 | "@typescript-eslint/eslint-plugin": "^8.28.0", 108 | "@valora/eslint-config-typescript": "^1.1.15", 109 | "@valora/prettier-config": "^0.0.1", 110 | "chalk": "^4.1.2", 111 | "eslint": "^8.57.1", 112 | "eslint-plugin-import": "^2.31.0", 113 | "eslint-plugin-jest": "^26.9.0", 114 | "eslint-plugin-jsx-expressions": "^1.3.2", 115 | "eslint-plugin-react": "^7.37.4", 116 | "eslint-plugin-react-hooks": "^4.6.2", 117 | "eslint-plugin-react-native": "^4.1.0", 118 | "knip": "^3.13.2", 119 | "patch-package": "^6.5.1", 120 | "postinstall-postinstall": "^2.1.0", 121 | "prettier": "^3.7.4", 122 | "shelljs": "^0.9.2", 123 | "ts-node": "^10.9.2", 124 | "tslib": "^2.8.1", 125 | "typescript": "^5.9.3", 126 | "yargs": "^17.7.2" 127 | }, 128 | "prettier": "@valora/prettier-config" 129 | } 130 | -------------------------------------------------------------------------------- /scripts/generate-release-notes.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | // This script is used to generate release notes for a given release. 3 | // It correctly ignores cherry-picked commits from previous patch releases. 4 | 5 | import chalk from 'chalk' 6 | import shell from 'shelljs' 7 | import yargs from 'yargs' 8 | import { hideBin } from 'yargs/helpers' 9 | import packageJson from '../package.json' 10 | 11 | interface Commit { 12 | hash: string 13 | type: string 14 | message: string 15 | } 16 | 17 | const argv = yargs(hideBin(process.argv)) 18 | .option('lastTag', { 19 | type: 'string', 20 | description: 'The last release tag (optional)', 21 | }) 22 | .option('toRef', { 23 | type: 'string', 24 | description: 'The reference to generate release notes up to', 25 | default: 'HEAD', 26 | }) 27 | .option('verbose', { 28 | type: 'boolean', 29 | description: 'Enable verbose logging', 30 | default: true, 31 | }) 32 | .parseSync() 33 | 34 | function log(message: string) { 35 | if (argv.verbose) { 36 | console.error(message) 37 | } 38 | } 39 | 40 | function getLastTag(): string { 41 | log(chalk.blue('Fetching the last tag...')) 42 | // Get all tags sorted by version, then filter for those that look like semantic version tags 43 | // Note: We can't use `git describe --tags --abbrev=0` because that returns the latest tag that is a direct ancestor of the current branch 44 | // But patch releases are not direct ancestors 45 | const result = shell.exec( 46 | 'git tag --sort=-v:refname | grep -E "^.*[0-9]+\\.[0-9]+\\.[0-9]+" | head -n 1', 47 | { 48 | silent: true, 49 | }, 50 | ) 51 | if (result.code !== 0) { 52 | console.error(chalk.red('Error fetching last tag:', result.stderr)) 53 | process.exit(1) 54 | } 55 | return result.stdout.trim() 56 | } 57 | 58 | function getTagPrefix(tag: string): string { 59 | const match = tag.match(/^(.*)v?\d+\.\d+\.\d+$/) 60 | return match ? match[1] : '' 61 | } 62 | 63 | function getCommits(lastTag: string, toRef: string): Commit[] { 64 | log(chalk.blue(`Fetching commits between ${lastTag} and ${toRef}...`)) 65 | const range = `${lastTag}..${toRef}` 66 | 67 | // Assume GNU toolchain, unless on darwin then assume BSD toolchain. 68 | let reverseCommand = 'tac' 69 | if (process.platform === 'darwin') { 70 | reverseCommand = 'tail -r' 71 | } 72 | const result = shell.exec( 73 | `git rev-list ${range} --oneline | ${reverseCommand}`, 74 | { silent: true }, 75 | ) 76 | 77 | if (result.code !== 0) { 78 | console.error(chalk.red('Error fetching git commits:', result.stderr)) 79 | process.exit(1) 80 | } 81 | 82 | return result.stdout 83 | .trim() 84 | .split('\n') 85 | .map((line) => { 86 | const [hash, ...messageParts] = line.split(' ') 87 | const message = messageParts.join(' ') 88 | const match = message.match(/^(\w+)(\(.*?\))?:/) 89 | const type = match ? match[1] : 'other' 90 | return { hash, type, message } 91 | }) 92 | } 93 | 94 | function generateMainReleaseNotes(commits: Commit[], version: string): string { 95 | const features = commits.filter((c) => c.type === 'feat') 96 | const fixes = commits.filter((c) => c.type === 'fix') 97 | const others = commits.filter((c) => !['feat', 'fix'].includes(c.type)) 98 | 99 | let notes = `# Summary 100 | 101 | We've updated the app to fix bugs, enhance our features, and improve overall performance. 102 | 103 | ` 104 | 105 | if (features.length > 0) { 106 | notes += `## Features 107 | 108 | ${features.map((c) => `${c.hash} ${c.message}`).join('\n')} 109 | 110 | ` 111 | } 112 | 113 | if (fixes.length > 0) { 114 | notes += `## Bug Fixes 115 | 116 | ${fixes.map((c) => `${c.hash} ${c.message}`).join('\n')} 117 | 118 | ` 119 | } 120 | 121 | if (others.length > 0) { 122 | notes += `## Other 123 | 124 | ${others.map((c) => `${c.hash} ${c.message}`).join('\n')} 125 | ` 126 | } 127 | 128 | return notes.trim() 129 | } 130 | 131 | function generatePatchReleaseNotes(commits: Commit[], version: string): string { 132 | const [major, minor, patch] = version.split('.') 133 | const previousVersion = `${major}.${minor}.${parseInt(patch) - 1}` 134 | 135 | return `# Summary 136 | 137 | This release is a patch on top of v${previousVersion}, with additional commits. 138 | 139 | ## Commits included 140 | 141 | ${commits.map((c) => `${c.hash} ${c.message}`).join('\n')} 142 | ` 143 | } 144 | 145 | function main() { 146 | const { lastTag: userProvidedLastTag, toRef } = argv 147 | 148 | const currentVersion = packageJson.version 149 | 150 | log(chalk.blue(`Generating release notes for ${currentVersion}...`)) 151 | 152 | const lastTag = userProvidedLastTag || getLastTag() 153 | log(chalk.green(`Using last tag: ${lastTag}`)) 154 | 155 | const tagPrefix = getTagPrefix(lastTag) 156 | log(chalk.green(`Detected tag prefix: "${tagPrefix}"`)) 157 | 158 | let commits = getCommits(lastTag, toRef) 159 | log(chalk.green(`Found ${commits.length} commits`)) 160 | 161 | // Determine if it's a patch release 162 | const isPatchRelease = currentVersion.split('.')[2] !== '0' 163 | 164 | if (!isPatchRelease) { 165 | // For main releases, remove cherry-picked commits from the previous main release 166 | const [major, minor] = currentVersion.split('.') 167 | const lastMainTag = `${tagPrefix}${major}.${parseInt(minor) - 1}.0` 168 | 169 | if (lastMainTag !== lastTag) { 170 | const possibleCherryPickedCommits = getCommits(lastMainTag, lastTag) 171 | const possibleCherryPickedMessages = new Set( 172 | possibleCherryPickedCommits.map((commit) => commit.message), 173 | ) 174 | const cherryPickedCommits = commits.filter((commit) => 175 | possibleCherryPickedMessages.has(commit.message), 176 | ) 177 | commits = commits.filter( 178 | (commit) => !possibleCherryPickedMessages.has(commit.message), 179 | ) 180 | log( 181 | chalk.yellow( 182 | `Possible cherry-picked commits: ${possibleCherryPickedCommits.length}`, 183 | ), 184 | ) 185 | log( 186 | chalk.yellow( 187 | `Skipping ${cherryPickedCommits.length} cherry-picked commits`, 188 | ), 189 | ) 190 | cherryPickedCommits.forEach((commit) => { 191 | log(chalk.yellow(` ${commit.hash} ${commit.message}`)) 192 | }) 193 | } 194 | } 195 | 196 | const releaseNotes = isPatchRelease 197 | ? generatePatchReleaseNotes(commits, currentVersion) 198 | : generateMainReleaseNotes(commits, currentVersion) 199 | 200 | // Output release notes to stdout 201 | console.log(releaseNotes) 202 | 203 | log(chalk.green('Release notes generated successfully')) 204 | } 205 | 206 | main() 207 | -------------------------------------------------------------------------------- /app.config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { version } = require('./package.json') 3 | const withAndroidAppThemeFullScreen = require('./plugins/withAndroidAppThemeFullScreen') 4 | const withCustomGradleProperties = require('./plugins/withCustomGradleProperties') 5 | const withDesugaring = require('./plugins/withDesugaring') 6 | 7 | const APP_REGISTRY_NAME = 'Valora' 8 | 9 | // Firebase credentials 10 | const GOOGLE_SERVICE_INFO_PLIST = 11 | process.env.GOOGLE_SERVICE_INFO_PLIST ?? 12 | `${process.env.PWD}/.eas/.env/GOOGLE_SERVICES_PLIST` 13 | const GOOGLE_SERVICES_JSON = 14 | process.env.GOOGLE_SERVICES_JSON ?? 15 | `${process.env.PWD}/.eas/.env/GOOGLE_SERVICES_JSON` 16 | const firebaseEnabled = 17 | fs.existsSync(GOOGLE_SERVICE_INFO_PLIST) && 18 | fs.existsSync(GOOGLE_SERVICES_JSON) 19 | 20 | module.exports = () => { 21 | const appVariant = process.env.APP_VARIANT ?? 'mainnet-dev' 22 | 23 | const getAppConfig = () => { 24 | switch (appVariant) { 25 | case 'mainnet': 26 | return { 27 | name: 'Valora', 28 | appStoreId: '1520414263', 29 | bundleId: 'co.clabs.valora', 30 | auth0Domain: 'auth.valora.xyz', 31 | } 32 | case 'mainnet-dev': 33 | return { 34 | name: 'Valora (dev)', 35 | appStoreId: '1520414263', 36 | bundleId: 'co.clabs.valora.dev', 37 | auth0Domain: 'auth.valora.xyz', 38 | } 39 | case 'mainnet-nightly': 40 | return { 41 | name: 'Valora (nightly)', 42 | appStoreId: '1599290566', 43 | bundleId: 'co.clabs.valora.nightly', 44 | auth0Domain: 'auth.valora.xyz', 45 | } 46 | default: 47 | throw new Error(`Unknown app variant: ${appVariant}`) 48 | } 49 | } 50 | 51 | const { name, appStoreId, bundleId, auth0Domain } = getAppConfig() 52 | 53 | return { 54 | expo: { 55 | name, 56 | slug: 'valora', 57 | // Main scheme should be first (see index.tsx) 58 | scheme: ['celo', 'wc'], 59 | version, 60 | orientation: 'portrait', 61 | icon: './assets/icon/icon.png', 62 | userInterfaceStyle: 'light', 63 | // disable for now as it causes an Android build error with react-native-auth0 64 | // See https://github.com/auth0/react-native-auth0/issues/879 65 | newArchEnabled: false, 66 | ios: { 67 | icon: './assets/icon/icon.png', 68 | splash: { 69 | image: './assets/splash/xxxhdpi.jpg', 70 | resizeMode: 'cover', 71 | backgroundColor: '#ffffff', 72 | }, 73 | supportsTablet: false, 74 | bundleIdentifier: bundleId, 75 | associatedDomains: ['applinks:vlra.app', 'applinks:valoraapp.com'], 76 | infoPlist: { 77 | NSCameraUsageDescription: 78 | 'Connecting your camera allows you to scan codes for payments.', 79 | NSContactsUsageDescription: 80 | 'Adding your contacts makes it easy to send and request payments with your friends.', 81 | NSPhotoLibraryAddUsageDescription: 82 | 'Connecting your photo library allows you to save your code to your photos.', 83 | NSPhotoLibraryUsageDescription: 84 | 'This is required for you to choose a profile picture.', 85 | NSUserTrackingUsageDescription: 86 | 'We use the advertising identifier to accurately attribute app installs from ad campaigns.', 87 | NSFaceIDUsageDescription: 88 | 'This is required for you to use Face ID to secure your account.', 89 | NSLocationWhenInUseUsageDescription: 90 | 'This app requires location access to provide location-based features.', 91 | CFBundleAllowMixedLocalizations: true, 92 | ITSAppUsesNonExemptEncryption: false, 93 | }, 94 | entitlements: { 95 | 'aps-environment': 'production', 96 | }, 97 | ...(firebaseEnabled && { 98 | googleServicesFile: GOOGLE_SERVICE_INFO_PLIST, 99 | }), 100 | }, 101 | android: { 102 | edgeToEdgeEnabled: false, 103 | adaptiveIcon: { 104 | foregroundImage: './assets/icon/adaptive-foreground.png', 105 | backgroundImage: './assets/icon/adaptive-background.png', 106 | }, 107 | splash: { 108 | backgroundColor: '#ffffff', 109 | resizeMode: 'cover', 110 | mdpi: './assets/splash/mdpi.jpg', 111 | hdpi: './assets/splash/hdpi.jpg', 112 | xhdpi: './assets/splash/xhdpi.jpg', 113 | xxhdpi: './assets/splash/xxhdpi.jpg', 114 | xxxhdpi: './assets/splash/xxxhdpi.jpg', 115 | }, 116 | package: bundleId, 117 | // IMPORTANT: to avoid react-native-keychain issues when reinstalling the app 118 | allowBackup: false, 119 | // App Links 120 | intentFilters: [ 121 | { 122 | action: 'VIEW', 123 | autoVerify: true, 124 | data: [ 125 | { scheme: 'https', host: 'valoraapp.com' }, 126 | { scheme: 'https', host: 'vlra.app' }, 127 | { pathPrefix: '/wc' }, 128 | { scheme: 'http' }, 129 | ], 130 | category: ['BROWSABLE', 'DEFAULT'], 131 | }, 132 | ], 133 | permissions: [ 134 | 'android.permission.CAMERA', 135 | 'android.permission.ACCESS_NETWORK_STATE', 136 | 'android.permission.INTERNET', 137 | 'android.permission.POST_NOTIFICATIONS', 138 | ], 139 | ...(firebaseEnabled && { 140 | googleServicesFile: GOOGLE_SERVICES_JSON, 141 | }), 142 | }, 143 | plugins: [ 144 | withAndroidAppThemeFullScreen, 145 | [ 146 | 'expo-font', 147 | { 148 | fonts: [ 149 | 'assets/fonts/Inter-Bold.ttf', 150 | 'assets/fonts/Inter-Medium.ttf', 151 | 'assets/fonts/Inter-Regular.ttf', 152 | 'assets/fonts/Inter-SemiBold.ttf', 153 | ], 154 | }, 155 | ], 156 | [ 157 | 'expo-build-properties', 158 | { 159 | android: { 160 | targetSdkVersion: 35, // Use 35 as edgeToEdge support is required for SDK 35+ 161 | }, 162 | ios: { 163 | // Minimum iOS version we support 164 | deploymentTarget: '15.1', 165 | useFrameworks: 'static', 166 | }, 167 | }, 168 | ], 169 | [ 170 | 'react-native-permissions', 171 | { 172 | iosPermissions: ['Camera', 'AppTrackingTransparency', 'Contacts'], 173 | }, 174 | ], 175 | [ 176 | 'react-native-auth0', 177 | { 178 | domain: auth0Domain, 179 | }, 180 | ], 181 | [ 182 | '@divvi/mobile', 183 | { 184 | // Used in the User-Agent header 185 | appName: APP_REGISTRY_NAME, 186 | }, 187 | ], 188 | [ 189 | withCustomGradleProperties, 190 | { 191 | 'org.gradle.jvmargs': '-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError', 192 | }, 193 | ], 194 | [ 195 | 'expo-camera', 196 | { 197 | recordAudioAndroid: false, 198 | }, 199 | ], 200 | ...(firebaseEnabled 201 | ? [ 202 | '@react-native-firebase/app', 203 | '@react-native-firebase/auth', 204 | '@react-native-firebase/messaging', 205 | ] 206 | : []), 207 | ...(process.env.SENTRY_AUTH_TOKEN 208 | ? [ 209 | [ 210 | '@sentry/react-native/expo', 211 | { 212 | organization: 'valora-inc', 213 | project: 'celo-mobile', 214 | url: 'https://sentry.io/', 215 | }, 216 | ], 217 | ] 218 | : []), 219 | withDesugaring, 220 | ], 221 | locales: { 222 | 'en-US': require('./locales/en-US.json'), 223 | 'es-419': require('./locales/es-419.json'), 224 | 'pt-BR': require('./locales/pt-BR.json'), 225 | de: require('./locales/de.json'), 226 | 'ru-RU': require('./locales/ru-RU.json'), 227 | 'fr-FR': require('./locales/fr-FR.json'), 228 | 'it-IT': require('./locales/it-IT.json'), 229 | 'uk-UA': require('./locales/uk-UA.json'), 230 | 'pl-PL': require('./locales/pl-PL.json'), 231 | 'th-TH': require('./locales/th-TH.json'), 232 | 'tr-TR': require('./locales/tr-TR.json'), 233 | 'vi-VN': require('./locales/vi-VN.json'), 234 | 'zh-CN': require('./locales/zh-CN.json'), 235 | }, 236 | extra: { 237 | registryName: APP_REGISTRY_NAME, 238 | appStoreId, 239 | auth0Domain, 240 | firebaseEnabled, 241 | eas: { 242 | projectId: '8593729d-4d16-40aa-b712-7f96b2293c9f', 243 | }, 244 | }, 245 | owner: 'divvi', 246 | }, 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Valora Inc 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /WALLET.md: -------------------------------------------------------------------------------- 1 | # Mobile (Valora) 2 | 3 | - [Mobile (Valora)](#mobile-valora) 4 | - [Overview](#overview) 5 | - [Architecture](#architecture) 6 | - [Setup](#setup) 7 | - [Prerequisites](#prerequisites) 8 | - [Repository secrets](#repository-secrets) 9 | - [For Valora employees only](#for-valora-employees-only) 10 | - [For External contributors](#for-external-contributors) 11 | - [iOS](#ios) 12 | - [Enroll in the Apple Developer Program](#enroll-in-the-apple-developer-program) 13 | - [Install Xcode](#install-xcode) 14 | - [Install Ruby, Cocoapods, Bundler, and download project dependencies](#install-ruby-cocoapods-bundler-and-download-project-dependencies) 15 | - [Install Rosetta (M1 macs only)](#install-rosetta-m1-macs-only) 16 | - [Android](#android) 17 | - [MacOS](#macos) 18 | - [Linux](#linux) 19 | - [Optional: Install an Android emulator](#optional-install-an-android-emulator) 20 | - [Configure an emulator using the Android SDK Manager](#configure-an-emulator-using-the-android-sdk-manager) 21 | - [Install Genymotion Emulator Manager](#install-genymotion-emulator-manager) 22 | - [MacOS](#macos-1) 23 | - [Linux](#linux-1) 24 | - [Running the mobile wallet](#running-the-mobile-wallet) 25 | - [iOS](#ios-1) 26 | - [Android](#android-1) 27 | - [Running on Mainnet](#running-on-mainnet) 28 | - [Reinstalling the app without building](#reinstalling-the-app-without-building) 29 | - [Debugging \& App Profiling](#debugging--app-profiling) 30 | - [Debugging](#debugging) 31 | - [Install Flipper](#install-flipper) 32 | - [App Profiling with react-devtools](#app-profiling-with-react-devtools) 33 | - [App Profiling with Android Profiler](#app-profiling-with-android-profiler) 34 | - [Testing](#testing) 35 | - [Snapshot testing](#snapshot-testing) 36 | - [React component unit testing](#react-component-unit-testing) 37 | - [Saga testing](#saga-testing) 38 | - [End-to-End testing](#end-to-end-testing) 39 | - [Building APKs / Bundles](#building-apks--bundles) 40 | - [Creating a fake keystore](#creating-a-fake-keystore) 41 | - [Building an APK or Bundle](#building-an-apk-or-bundle) 42 | - [Other](#other) 43 | - [Localization (l10n) / translation process](#localization-l10n--translation-process) 44 | - [Configuring the SMS Retriever](#configuring-the-sms-retriever) 45 | - [Redux state migration](#redux-state-migration) 46 | - [When is a migration or new schema version needed?](#when-is-a-migration-or-new-schema-version-needed) 47 | - [What do to when test/RootStateSchema.json needs an update?](#what-do-to-when-testrootstateschemajson-needs-an-update) 48 | - [Redux-Saga pitfalls](#redux-saga-pitfalls) 49 | - [Error bubbling](#error-bubbling) 50 | - [Why do we use http(s) provider?](#why-do-we-use-https-provider) 51 | - [Helpful hints for development](#helpful-hints-for-development) 52 | - [Vulnerabilities found in dependencies](#vulnerabilities-found-in-dependencies) 53 | - [Branding](#branding) 54 | - [Troubleshooting](#troubleshooting) 55 | - [Postinstall script](#postinstall-script) 56 | - [`Activity class {org.celo.mobile.staging/org.celo.mobile.MainActivity} does not exist.`](#activity-class-orgcelomobilestagingorgcelomobilemainactivity-does-not-exist) 57 | - [Podfile changes not picked up by iOS build](#podfile-changes-not-picked-up-by-ios-build) 58 | 59 | ## Overview 60 | 61 | This package contains the code for the Valora mobile apps for Android and iOS. 62 | Valora is a self-sovereign wallet that enables anyone to onboard onto the Celo network, manage their currencies, and send payments. 63 | 64 | ## Architecture 65 | 66 | The app uses [React Native][react native]. 67 | 68 | ## Setup 69 | 70 | ### Prerequisites 71 | 72 | Install [Homebrew](https://brew.sh) if you are on macOS. 73 | 74 | Install [NVM](https://github.com/nvm-sh/nvm#install--update-script) if you don't have any Node version manager. 75 | 76 | Install Node version listed in [.nvmrc](.nvmrc) and make it default (example for NVM): 77 | 78 | ```bash 79 | nvm install --default 80 | ``` 81 | 82 | Install Yarn 83 | 84 | ```bash 85 | npm install --global yarn 86 | ``` 87 | 88 | Install [watchman][watchman] and [jq][jq] 89 | 90 | ```bash 91 | # On a mac 92 | brew install watchman 93 | brew install jq 94 | ``` 95 | 96 | ### Repository secrets 97 | 98 | #### For Valora employees only 99 | 100 | _This is only for Valora employees._ 101 | 102 | You will need to be added the team keyring on GCP so you can decrypt secrets in the repo. (Ask for an invite to `celo-mobile-alfajores`.) 103 | 104 | Once you have access, install Google Cloud by running `brew install google-cloud-sdk`. 105 | Follow instructions [here](https://cloud.google.com/sdk/gcloud/reference/auth/login) 106 | for logging in with Google credentials. 107 | 108 | To test your GCP access, try running `yarn keys:decrypt` from the wallet repo root. You should see something like this: `Encrypted files decrypted`. 109 | (You will not need to run this command on an ongoing basis, since it is done automatically as part of the `postinstall` script.) 110 | 111 | #### For External contributors 112 | 113 | External contributors don't need to decrypt repository secrets and can successfully build and run the mobile application with the following differences: 114 | 115 | - the default branding will be used (some images/icons will appear in pink or will be missing) 116 | - Firebase related features need to be disabled. You can do this by setting `FIREBASE_ENABLED=false` in the `.env.*` files. 117 | 118 | ### iOS 119 | 120 | #### Enroll in the Apple Developer Program 121 | 122 | In order to successfully set up your iOS development environment you will need to enroll in the [Apple Developer Program]. It is recommended that you enroll from an iOS device by downloading the Apple Developer App in the App Store. Using the app will result in the fastest processing of your enrollment. 123 | 124 | _If you are a Valora employee, please ask to be added to the Valora iOS development team._ 125 | 126 | #### Install Xcode 127 | 128 | Xcode is needed to build and deploy the mobile wallet to your iOS device. If you do not have an iOS device, Xcode can be used to emulate one. 129 | 130 | Install [Xcode 15.2](https://developer.apple.com/download/more/?q=xcode) (an Apple Developer Account is needed to access this link). 131 | 132 | We do not recommend installing Xcode through the App Store as it can auto update and become incompatible with our projects. 133 | 134 | Note that using the method above, you can have multiple versions of Xcode installed in parallel if you'd like. Simply use different names for the different version of Xcode in your computer's `Applications` folder (e.g., `Xcode14.3.1.app` and `Xcode15.2.app`). 135 | 136 | #### Install Ruby, Cocoapods, Bundler, and download project dependencies 137 | 138 | Install Ruby 2.7 and make it global 139 | 140 | ```bash 141 | brew install rbenv ruby-build 142 | 143 | # run and follow the printed instructions: 144 | rbenv init 145 | 146 | rbenv install 2.7.8 147 | rbenv global 2.7.8 148 | ``` 149 | 150 | Make sure you are in the `ios` directory of the repository root before running the following: 151 | 152 | ```bash 153 | # install cocopods and bundler if you don't already have it 154 | gem install cocoapods 155 | gem install bundler 156 | # download the project dependencies in repository root 157 | bundle install 158 | # run inside /ios 159 | bundle exec pod install 160 | ``` 161 | 162 | 1. Run `yarn install` in the repository root. 163 | 2. Run `yarn dev:ios` in the repository root. 164 | 165 | #### Install Rosetta (M1 macs only) 166 | 167 | If you are unable to run the app in the iOS Simulator, install Rosetta: 168 | 169 | ```bash 170 | /usr/sbin/softwareupdate --install-rosetta --agree-to-license 171 | ``` 172 | 173 | ### Android 174 | 175 | [Download and install Android Studio](https://developer.android.com/studio/index.html) and the following add-ons: 176 | 177 | - Android SDK 178 | - Android SDK Platform 179 | - Android Virtual Device 180 | 181 | Install the Android 13 (Tiramisu) SDK. It can be found can be installed through the SDK Manager in Android Studio. 182 | 183 | Configure the `ANDROID_HOME` environment variables by adding the following lines to your `~/.zprofile` or `~/.zshrc`. You can find the actual location of the SDK in the Android Studio "Preferences" dialog, under Appearance & Behavior → System Settings → Android SDK. 184 | 185 | ```bash 186 | export ANDROID_HOME=$HOME/Library/Android/sdk 187 | export PATH=$PATH:$ANDROID_HOME/emulator 188 | export PATH=$PATH:$ANDROID_HOME/platform-tools 189 | ``` 190 | 191 | #### MacOS 192 | 193 | After installing Andoid Studio, add the [Android NDK][android ndk] (if you run into issues with the toolchain, try using version: 22.x). 194 | 195 | Make sure these lines are in your shell profile (`~/.bash_profile`, `~/.zshrc` etc.): 196 | 197 | _Note that these paths may differ on your machine. You can find the path to the SDK and NDK via the [Android Studio menu](https://stackoverflow.com/questions/40520324/how-to-find-the-path-to-ndk)._ 198 | 199 | ```bash 200 | export ANDROID_HOME=$HOME/Library/Android/sdk 201 | export PATH=$PATH:$ANDROID_HOME/emulator 202 | export PATH=$PATH:$ANDROID_HOME/platform-tools 203 | export ANDROID_NDK=$ANDROID_HOME/ndk-bundle 204 | export ANDROID_SDK_ROOT=$ANDROID_HOME 205 | # this is an optional gradle configuration that should make builds faster 206 | export GRADLE_OPTS='-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"' 207 | export TERM_PROGRAM=iterm # or whatever your favorite terminal program is 208 | ``` 209 | 210 | (optional) You may want to install Jenv to manage multiple Java versions: 211 | 212 | ```bash 213 | brew install jenv 214 | eval "$(jenv init -)" 215 | # next step assumes jdk already installed 216 | jenv add /Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home 217 | ``` 218 | 219 | #### Linux 220 | 221 | Install Java by running the following: 222 | 223 | ``` 224 | sudo apt install openjdk-11-jdk 225 | ``` 226 | 227 | You can download the complete Android Studio and SDK from the [Android Developer download site](https://developer.android.com/studio/#downloads). 228 | 229 | You can find the complete instructions about how to install the tools in Linux environments in the [Documentation page](https://developer.android.com/studio/install#linux). 230 | 231 | Set the following environment variables and optionally add to your shell profile (_e.g._, `.bash_profile`): 232 | 233 | ```bash 234 | export ANDROID_HOME=/usr/local/share/android-sdk 235 | export ANDROID_SDK_ROOT=/usr/local/share/android-sdk 236 | # this is an optional gradle configuration that should make builds faster 237 | export GRADLE_OPTS='-Dorg.gradle.daemon=true -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs="-Xmx4096m -XX:+HeapDumpOnOutOfMemoryError"' 238 | # this is used to launch the react native packager in its own terminal 239 | export TERM_PROGRAM=xterm # or whatever your favorite terminal is 240 | ``` 241 | 242 | ##### Optional: Install an Android emulator 243 | 244 | ##### Configure an emulator using the Android SDK Manager 245 | 246 | Set your `PATH` environment variable and optionally update your shell profile (_e.g._, `.bash_profile`): 247 | 248 | ```bash 249 | export PATH=$ANDROID_HOME/emulator:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH 250 | ``` 251 | 252 | Install the Android 31 system image and create an Android Virtual Device. 253 | 254 | ###### For Intel chip Macs: 255 | 256 | ```bash 257 | sdkmanager "system-images;android-31;default;x86_64" 258 | avdmanager create avd --force --name Pixel_API_31_AOSP_x86_64 --device pixel -k "system-images;android-31;default;x86_64" 259 | ``` 260 | 261 | ###### For M1 Macs: 262 | 263 | On an M1 mac, the above commands may succeed, but when you try to run the emulator it will fail saying you have an unsupported architecture. To get around this, you can manually create the Android Virtual Device in Android Studio by doing the following: 264 | 265 | - Open the wallet repo in Android Studio. In the top bar, click Tools -> Device Manager. A side-bar should pop up on your screen showing your virtual devices (if any). 266 | - Click "Create Device", choose a device (e.g. Pixel 6 Pro), and hit next. 267 | - When prompted to select a system image, click the "ARM Images" tab, and choose an image where the target **does not** include "Google APIs". You may need to download the system image, to do that hit the download icon next to the release name. For e2e testing, choose release name "Q" a.k.a. Android 10. 268 | - Give the device a name, and hit finish If you are creating a virtual device for e2e testing purposes, name your device `Pixel_API_29_AOSP_x86_64`. 269 | 270 | Run the emulator with: 271 | 272 | ```bash 273 | emulator -avd 274 | ``` 275 | 276 | ##### Install Genymotion Emulator Manager 277 | 278 | Another Android emulator option is Genymotion. 279 | 280 | ###### MacOS 281 | 282 | ```bash 283 | brew install --cask genymotion 284 | ``` 285 | 286 | Under OSX High Sierra and later, you'll get a message that you need to 287 | [approve it in System Preferences > Security & Privacy > General][approve kernel extension]. 288 | 289 | Do that, and then repeat the line above. 290 | 291 | Then make sure the ADB path is set correctly in Genymotion — set 292 | `Preferences > ADB > Use custom Android SDK tools` to 293 | `/usr/local/share/android-sdk` (same as `$ANDROID_HOME`) 294 | 295 | ###### Linux 296 | 297 | You can download the Linux version of Genymotion from the [fun zone!](https://www.genymotion.com/fun-zone/) (you need to sign in first). 298 | 299 | After having the binary you only need to run the installer: 300 | 301 | ``` 302 | sudo ./genymotion-3.0.2-linux_x64.bin 303 | ``` 304 | 305 | ## Running the mobile wallet 306 | 307 | The below steps should help you successfully run the mobile wallet on either a USB connected or emulated device. For additional information and troubleshooting see the [React Native docs][rn running on device]. 308 | 309 | **Note:** We've seen some issues running the metro bundler from iTerm 310 | 311 | 1. If you haven't already, run `yarn` and then `yarn build` from the repository root to install and build dependencies. 312 | 313 | 2. Attach your device or start an emulated one. 314 | 315 | ### iOS 316 | 317 | 3. Launch Xcode and use it to open the directory `MobileStack.xcworkspace`. Confirm your iOS device has been detected by Xcode. 318 | 319 | 4. Build the project by pressing the play button in the top left corner or selecting `Product > Build` from the Xcode menu bar. 320 | 321 | 5. From the repository root directory run `yarn run dev:ios`. 322 | 323 | ### Android 324 | 325 | 3. Follow [these instructions to enable Developer Options][android dev options] on your Android device. 326 | 327 | 4. Unplug and replug your device. You'll be prompted to accept the connection and shown a public key (corresponding to the `abd_key.pub` file in `~/.android`) 328 | 329 | 5. To confirm your device is properly connected, running `adb devices` from the terminal should reflect your connected device. If it lists a device as "unauthorized", make sure you've accepted the prompt or [troubleshoot here][device unauthorized]. 330 | 331 | 6. From the repository root directory run `yarn run dev:android`. 332 | 333 | ### Running on Mainnet 334 | 335 | By default, the mobile wallet app runs on celo's testnet `alfajores`. To run the app on `mainnet`, supply an env flag, eg. `yarn run dev:ios -e mainnet`. The command will then run the app with the env file `.env.mainnet`. 336 | 337 | ### Reinstalling the app without building 338 | 339 | To test some scenarios (e.g., native permissions modals which appear only once), 340 | you may require a fresh install of the app. Instead of rebuilding the app to get 341 | a fresh install, you can drag drop the generated app into the simulator after 342 | uninstalling the app. It is typically available in the following paths: 343 | 344 | - For iOS: `$HOME/Library/Developer/Xcode/DerivedData/MobileStack-/Build/Products/Debug-iphonesimulator/Valora.app` 345 | - For Android: `/android/app/build/outputs/apk/alfajoresdev/debug/app-alfajoresdev-debug.apk` 346 | 347 | ## Debugging & App Profiling 348 | 349 | ### Debugging 350 | 351 | Since we integrated dependencies making use of TurboModules, debugging via Chrome DevTools or React Native Debugger doesn't work anymore. 352 | As an alternative, Flipper can be used instead. 353 | 354 | #### Install Flipper 355 | 356 | [Flipper][flipper] is a platform for debugging iOS, Android and React Native apps. Visualize, inspect, and control your apps from a simple desktop interface. Download on the web or through brew. 357 | 358 | ```sh 359 | brew install flipper 360 | ``` 361 | 362 | As of Jan 2021, Flipper is not notarized and triggers a MacOS Gatekeeper popup when trying to run it for the first time. 363 | Follow [these steps to successfully launch it](https://github.com/facebook/flipper/issues/1308#issuecomment-652951556) (only needed the very first time it's run) 364 | 365 | The application currently makes use of 2 additional Flipper plugins to enable more detailed debugging: 366 | 367 | - Redux Debugger (Flipper -> Manage Plugins -> Install Plugins -> search redux-debugger) 368 | - React Navigation (Flipper -> Manage Plugins -> Install Plugins -> search react-navigation) 369 | 370 | Once installed, you should be able to see them and interact with them when the wallet is running (only in dev builds). 371 | 372 | This allows viewing / debugging the following: 373 | 374 | - React DevTools (Components and Profiling) 375 | - Network connections 376 | - View hierarchy 377 | - Redux State / Actions 378 | - Navigation State 379 | - AsyncStorage 380 | - App preferences 381 | - Hermes 382 | - and more ;) 383 | 384 | If you're using an Android simulator and the device / app is not showing up, 385 | navigate to settings (gear icon in the bottom left) and ensure the Android SDK 386 | location points to the same location as the $ANDROID_HOME environment variable. 387 | 388 | ### App Profiling with react-devtools 389 | 390 | From Flipper select React DevTools Plugin while the app is running locally, or run `yarn run react-devtools` in the wallet root folder. It should automatically connect to the running app and includes a profiler (second tab). Start recording with the profiler, use the app and then stop recording. If running from the terminal, Flipper cannot be run at the same time. 391 | 392 | The flame graph provides a view of each component and sub-component. The width is proportional to how long it took to load. If it is grey, it was not re-rendered at that 'commit' or DOM change. Details on the react native profiler are [here][rn profiler]. The biggest thing to look for are large number of renders when no state has changed. Reducing renders can be done via pure components in React or overloading the should component update method [example here][rn optimize example]. 393 | 394 | ### App Profiling with Android Profiler 395 | 396 | Profiling in release mode is recommended because memory usage tends to be significantly higher in development builds. To create a local mainnet release build for profiling, use the following command: `yarn dev:android -e mainnet -r -t`. This supplies an env flag: `-e `, the release flag: `-r` and the profile flag: `-t`. 397 | 398 | To analyze the app's memory, CPU, and energy usage, the [Android APK Profiler][androidprofilerapk] is a useful tool. In Android Studio, navigate to `File > Profile or Debug APK`, then select the APK built in the previous step, typically located at `android/app/build/outputs/apk/mainnet/release`. Once both the app and Android Studio are running, attach a new profiling session by selecting your device and choosing the debuggable process, such as co.clabs.valora. 399 | 400 | ## Testing 401 | 402 | To execute the suite of tests, run `yarn test`. 403 | 404 | ### Snapshot testing 405 | 406 | We use Jest [snapshot testing][jest] to assert that no intentional changes to the 407 | component tree have been made without explicit developer intention. See an 408 | example at [`src/send/SendAmount.test.tsx`]. If your snapshot is expected 409 | to deviate, you can update the snapshot with the `-u` or `--updateSnapshot` 410 | flag when running the test. 411 | 412 | ### React component unit testing 413 | 414 | We use [react-native-testing-library][react-native-testing-library] and [@testing-library/jest-native][@testing-library/jest-native] to unit test 415 | react components. It allows for deep rendering and interaction with the rendered 416 | tree to assert proper reactions to user interaction and input. See an example at 417 | [`src/send/SendAmount.test.tsx`] or read more about the [docs][rntl-docs]. 418 | 419 | To run a single component test file: `yarn test Send.test.tsx` 420 | 421 | ### Saga testing 422 | 423 | We use [redux-saga-test-plan][redux-saga-test-plan] to test complex sagas. 424 | See [`src/app/saga.test.ts`] for an example. 425 | 426 | ### End-to-End testing 427 | 428 | We use [Detox][detox] for E2E testing. In order to run the tests locally, you 429 | must have the proper emulator set up. Follow the instructions in [e2e/README.md][e2e readme]. 430 | 431 | Once setup is done, you can build the tests with `yarn e2e:build:android-release` or `yarn e2e:build:ios-release`. 432 | Once test build is done, you can run the tests with `yarn e2e:test:android-release` or `yarn e2e:test:ios-release`. 433 | If you want to run a single e2e test: `yarn e2e:test:ios-release Exchange.spec.js -t "Then Buy CELO"` 434 | 435 | ## Building APKs / Bundles 436 | 437 | You can create your own custom build of the app via the command line or in Android Studio. For an exact set of commands, refer to the lanes in `fastlane/FastFile`. For convenience, the basic are described below: 438 | 439 | ### Creating a fake keystore 440 | 441 | If you have not yet created a keystore, one will be required to generate a release APKs / bundles: 442 | 443 | ```sh 444 | cd android/app 445 | keytool -genkey -v -keystore mobilestack-release-key.keystore -alias mobilestack-key-alias -storepass fakeReleaseStorePass -keypass fakeReleaseStorePass -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US" 446 | export RELEASE_STORE_PASSWORD=fakeReleaseStorePass 447 | ``` 448 | 449 | ### Building an APK or Bundle 450 | 451 | ```sh 452 | # With fastlane: 453 | bundle install 454 | bundle exec fastlane android build_apk env:YOUR_BUILDING_VARIANT 455 | 456 | # Or, manually 457 | cd android/ 458 | ./gradlew clean 459 | ./gradlew bundle{YOUR_BUILDING_VARIANT}JsAndAssets 460 | # For an APK: 461 | ./gradlew assemble{YOUR_BUILDING_VARIANT} -x bundle{YOUR_BUILDING_VARIANT}JsAndAssets 462 | # Or for a bundle: 463 | ./gradlew bundle{YOUR_BUILDING_VARIANT} -x bundle{YOUR_BUILDING_VARIANT}JsAndAssets 464 | ``` 465 | 466 | Where `YOUR_BUILD_VARIANT` can be any of the app's build variants, such as debug or release. 467 | 468 | ## Other 469 | 470 | ### Localization (l10n) / translation process 471 | 472 | We are using [Crowdin](https://clabs.crowdin.com/) to manage the translation of all user facing strings in the app. 473 | 474 | During development, developers should only update the language files in the base locale. These are the source files for Crowdin. 475 | 476 | The `main` branch of this repository is automatically synced with our Crowdin project. Source files in Crowdin are updated automatically and ready translations are pushed as a pull request. 477 | 478 | Translation process overview: 479 | 480 | 1. Developers update the base strings in English (in `locales/base`) in the branch they are working on. 481 | 1. When the corresponding PR is merged into `main`, Crowdin integration automatically picks up changes to the base strings. 482 | 1. Crowdin then auto translates the new strings and opens a PR with them from the `l10n/main` branch 483 | 1. We can then manually check and edit the translated strings in the Crowdin UI. The changes will be reflected in the PR after 10 mins. 484 | 1. When we are happy with the changes, we can merge the PR and delete the related `l10n/main` branch to avoid possible future conflicts. Once new translations are made in Crowdin, a new `l10n/main` branch will be automatically created again. 485 | 486 | When making a release, we should make sure there are no outstanding translation changes not yet merged into `main`. 487 | i.e. no Crowdin PR open and the translation status for all supported languages is at 100% and approved on Crowdin. 488 | 489 | Note that Crowdin Over-The-Air (OTA) content delivery is used to push live translation updates to the app. As only target languages are included in the Crowdin OTA distribution, English is set up as a target language as well as the source. This is a necessary implementation detail to prevent bi-directional sync between Crowdin and Github. The translated English strings (in `locales/en`) are only to receive the OTA translations, and it is not necessary to consume or edit them otherwise within the app. 490 | 491 | ### Configuring the SMS Retriever 492 | 493 | On Android, the wallet app uses the SMS Retriever API to automatically input codes during phone number verification. When creating a new app build type this needs to be properly configured. 494 | 495 | The service that routes SMS messages to the app needs to be configured to [append this app signature to the message][sms retriever]. The hash depends on both the bundle id and the signing certificate. Since we use Google Play signing, we need to download the certificate. 496 | 497 | 1. Go to the play console for the relevant app, Release management > App signing, and download the App signing certificate. 498 | 2. Use this script to generate the hash code: https://github.com/michalbrz/sms-retriever-hash-generator 499 | 500 | ### Redux state migration 501 | 502 | We're using [redux-persist](https://github.com/rt2zz/redux-persist) to persist the state of the app across launches. 503 | 504 | Whenever we add/remove/update properties to the [RootState][rootstate], we need to ensure previous versions of the app can successfully migrate their persisted state to the new schema version. 505 | Otherwise it can lead to subtle bugs or crashes for existing users of the app, when their app is upgraded. 506 | 507 | We have automated checks to ensure that the state migration is working correctly across all versions. You're probably reading this because these checks pointed you to this documentation. 508 | These checks are based on the JSON schema representation of the [RootState][rootstate] TypeScript type. It is stored in [test/RootStateSchema.json][rootstateschema]. 509 | 510 | #### When is a migration or new schema version needed? 511 | 512 | As a rule of thumb, a migration is needed whenever the [RootState][rootstate] changes. That is whenever [test/RootStateSchema.json][rootstateschema] changes. 513 | 514 | Here we're optimizing for correctness and explicitness to avoid breaking existing users. 515 | 516 | [redux-persist](https://github.com/rt2zz/redux-persist) can automatically handle newly added properties with its [state reconcilier](https://github.com/rt2zz/redux-persist#state-reconciler). 517 | However it leaves removed properties. Which is fine in the majority of the cases, but could create issues if later on a property is added again with the same name. 518 | And it only merges the initial state with the persisted state up to 2 levels of nesting (this is the `autoMergeLevel2` config we are using). 519 | 520 | So in general, if you're only adding a new reducer or adding a new property to an existing reducer, the migration can just return the input state. The state reconciler will do the right thing. 521 | 522 | If you're deleting or updating existing properties, please implement the appropriate migration for them. 523 | 524 | #### What do to when [test/RootStateSchema.json][rootstateschema] needs an update? 525 | 526 | 1. Run `yarn test:update-root-state-schema`. This will ensure the JSON schema is in sync with the [RootState][rootstate] TypeScript type. 527 | 2. Review the changes in the schema 528 | 3. Increase the schema version in [src/redux/store.ts](src/redux/store.ts#L27) 529 | 4. Add a new migration in [src/redux/migrations.ts](src/redux/migrations.ts) 530 | 5. Add a new test schema in [test/schema.ts](test/schema.ts), with the newly added/deleted/updated properties. The test schema is useful to test migrations and show how the schema changed over time. 531 | 6. Optional: if the migration is not trivial, add a test for it in [src/redux/migrations.test.ts](src/redux/migrations.test.ts) 532 | 7. Commit the changes 533 | 534 | ### Redux-Saga pitfalls 535 | 536 | ### Error bubbling 537 | 538 | It's important to understand how errors propagate with Redux-Saga. 539 | 540 | Take the following example: 541 | 542 | ```ts 543 | function* rootSaga() { 544 | yield spawn(mySaga) 545 | yield spawn(someOtherSaga) 546 | } 547 | 548 | function* mySaga() { 549 | yield takeEvery('SEND_PAYMENT', sendPayment) 550 | yield takeEvery('NOTIFY_USER', notifyUser) 551 | } 552 | 553 | function* someOtherSaga() { 554 | // [...] 555 | } 556 | ``` 557 | 558 | If an exception is thrown from `sendPayment` or `notifyUser`, the whole `mySaga` will be cancelled. 559 | And won't handle `SEND_PAYMENT` AND `NOTIFY_USER` actions until the app is restarted. 560 | 561 | Since `mySaga` was spawned from the root saga, `someOtherSaga` won't be affected though. 562 | 563 | You may think that a good way to address this problem is to make sure `sendPayment` uses `try/catch`. 564 | 565 | ```ts 566 | function* sendPayment() { 567 | try { 568 | // Code to send payment 569 | // [...] 570 | } catch (e) { 571 | Logger.error(e) 572 | yield put('SEND_PAYMENT_FAILED') 573 | } 574 | } 575 | ``` 576 | 577 | However it's still possible that the `catch` block throws again, and we'd be back to the initial problem. 578 | 579 | To avoid this problem, we recommend wrapping `takeEvery`/`takeLatest`/`takeLeading` worker sagas using the [`safely`](src/utils/safely.ts) helper. 580 | 581 | Note that you should still handle errors happening in your action handlers. But at least you'll have the guarantee that it won't unexpectedly stop listening to actions because of an unhandled error. 582 | 583 | See more details https://redux-saga.js.org/docs/api#error-propagation 584 | 585 | ### Why do we use http(s) provider? 586 | 587 | Websockets (`ws`) would have been a better choice but we cannot use unencrypted `ws` provider since it would be bad to send plain-text data from a privacy perspective. Geth does not support `wss` by [default](https://github.com/ethereum/go-ethereum/issues/16423). And Kubernetes does not support it either. This forced us to use https provider. 588 | 589 | ### Helpful hints for development 590 | 591 | We try to minimise the differences between running Valora in different modes and environments, however there are a few helpful things to know when developing the app. 592 | 593 | - Valora uses Crowdin Over-The-Air (OTA) content delivery to enable dynamic translation updates. The OTA translations are cached and used on subsequent app loads instead of the strings in the translation files of the app bundle. This means that during development, the app will not respond to manual changes of the translation.json files. 594 | - In development mode, analytics are disabled. 595 | 596 | ### Vulnerabilities found in dependencies 597 | 598 | We have a script to [check for vulnerabilities](scripts/ci_check_vulnerabilities.sh) in our dependencies. 599 | 600 | The script reports all vulnerabilities found; compare its output with [yarn-audit-known-issues](/yarn-audit-known-issues) to see which ones are new. 601 | 602 | In case vulnerabilities are reported, check to see if they apply to production and if they have fixes available. 603 | 604 | If they apply to production, start a discussion in our [#on-call](https://valora-app.slack.com/archives/C02N3AR2P2S) channel. 605 | 606 | Then if they have fixes available, update the dependencies using [Renovate](https://github.com/valora-inc/wallet/issues/1716) or manually: 607 | 608 | - If it's a direct dependency, update the dependency in `package.json`. 609 | - If it's a transitive dependency, you can manually remove the transitive dependency in `yarn.lock` and re-run `yarn install` to see if it can use the fixed version. If the sub dependency is pinned somewhere, you'll need to use a [yarn resolution](https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/) in `package.json` to get the fixed version. Be careful with this as it can break other dependencies depending on a specific version. 610 | 611 | If they do not have fixes and they do not apply to production, you may ignore them: 612 | 613 | 1. run: `yarn audit --json --groups dependencies --level high | grep auditAdvisory > yarn-audit-known-issues` 614 | 2. commit `yarn-audit-known-issues` and open a PR 615 | 616 | ### Branding 617 | 618 | Images related to the brand are stored in the `src/images` folder. When adding new images, we also include the 1.5x, 2x, 3x, and 4x versions. The app will automatically download the appropriate size. 619 | 620 | ### Troubleshooting 621 | 622 | #### Postinstall script 623 | 624 | If you're having an error with installing packages, or `secrets.json` not existing: 625 | 626 | try to run `yarn postinstall` in the wallet root folder after running `yarn install`. 627 | 628 | If some of your assets are not loaded and you see an error running sync_branding.sh. 629 | Check if you have set up your Github connection with SSH. 630 | 631 | A successful `yarn postinstall` looks like: 632 | 633 | ``` 634 | $ yarn postinstall 635 | yarn run v1.22.17 636 | $ patch-package && yarn keys:decrypt && yarn run unzipCeloClient && ./scripts/sync_branding.sh && ./scripts/copy_license_to_android_assets.sh 637 | patch-package 6.2.2 638 | Applying patches... 639 | @react-native-community/netinfo@5.8.0 ✔ 640 | bn.js@4.11.9 ✔ 641 | react-native-sms@1.11.0 ✔ 642 | react-native-splash-screen@3.3.0 ✔ 643 | react-native-svg@12.1.1 ✔ 644 | react-native-tab-view@2.15.2 ✔ 645 | $ bash scripts/key_placer.sh decrypt 646 | Processing encrypted files 647 | Encrypted files decrypted 648 | . 649 | ~/src/github.com/valora-inc/wallet/branding/valora ~/src/github.com/valora-inc/wallet 650 | HEAD is now at ec0637b fix: update valora forum link (#9) 651 | ~/src/github.com/valora-inc/wallet 652 | Using branding/valora 653 | building file list ... done 654 | 655 | sent 7697 bytes received 20 bytes 15434.00 bytes/sec 656 | total size is 8003608 speedup is 1037.14 657 | building file list ... done 658 | 659 | sent 91 bytes received 20 bytes 222.00 bytes/sec 660 | total size is 1465080 speedup is 13198.92 661 | ✨ Done in 12.77s. 662 | ``` 663 | 664 | #### `Activity class {org.celo.mobile.staging/org.celo.mobile.MainActivity} does not exist.` 665 | 666 | From time to time the app refuses to start showing this error: 667 | 668 | ```text 669 | 557 actionable tasks: 525 executed, 32 up-to-date 670 | info Running /usr/local/share/android-sdk/platform-tools/adb -s PL2GARH861213542 reverse tcp:8081 tcp:8081 671 | info Starting the app on PL2GARH861213542 (/usr/local/share/android-sdk/platform-tools/adb -s PL2GARH861213542 shell am start -n org.celo.mobile.staging/org.celo.mobile.MainActivity)... 672 | Starting: Intent { cmp=org.celo.mobile.staging/org.celo.mobile.MainActivity } 673 | Error type 3 674 | Error: Activity class {org.celo.mobile.staging/org.celo.mobile.MainActivity} does not exist. 675 | ``` 676 | 677 | Solution: 678 | 679 | ```bash 680 | $ adb kill-server && adb start-server 681 | * daemon not running; starting now at tcp:5037 682 | * daemon started successfully 683 | ``` 684 | 685 | #### Podfile changes not picked up by iOS build 686 | 687 | Some [Podfile](ios/Podfile) changes may not be picked up by an iOS build (e.g., 688 | add new permissions with react-native-permissions [here](ios/Podfile#L37)) and 689 | will need cleaning the XCode derived data folder. 690 | 691 | ```console 692 | rm -rf $HOME/Library/Developer/Xcode/DerivedData/* 693 | ``` 694 | 695 | [celo platform]: https://celo.org 696 | [wallet]: https://github.com/valora-inc/wallet 697 | [celo-blockchain]: https://github.com/celo-org/celo-blockchain 698 | [apple developer program]: https://developer.apple.com/programs/ 699 | [detox]: https://github.com/wix/Detox 700 | [e2e readme]: ./e2e/README.md 701 | [protocol readme]: ../protocol/README.md 702 | [react native]: https://facebook.github.io/react-native/ 703 | [flipper]: https://fbflipper.com 704 | [rn optimize example]: https://reactjs.org/docs/optimizing-performance.html#examples 705 | [rn profiler]: https://reactjs.org/blog/2018/09/10/introducing-the-react-profiler.html 706 | [rn running on device]: https://facebook.github.io/react-native/docs/running-on-device 707 | [setup]: ../../SETUP.md 708 | [react-native-testing-library]: https://github.com/callstack/react-native-testing-library 709 | [@testing-library/jest-native]: https://github.com/testing-library/jest-native#readme 710 | [rntl-docs]: https://callstack.github.io/react-native-testing-library/docs/getting-started 711 | [jest]: https://jestjs.io/docs/en/snapshot-testing 712 | [redux-saga-test-plan]: https://github.com/jfairbank/redux-saga-test-plan 713 | [sms retriever]: https://developers.google.com/identity/sms-retriever/verify#1_construct_a_verification_message 714 | [android dev options]: https://developer.android.com/studio/debug/dev-options 715 | [android ndk]: https://developer.android.com/studio/projects/install-ndk 716 | [android studio]: https://developer.android.com/studio 717 | [approve kernel extension]: https://developer.apple.com/library/content/technotes/tn2459/_index.html 718 | [device unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized 719 | [watchman]: https://facebook.github.io/watchman/docs/install/ 720 | [jq]: https://stedolan.github.io/jq/ 721 | [rootstate]: src/redux/reducers.ts#L79 722 | [rootstateschema]: test/RootStateSchema.json 723 | [androidprofilerapk]: https://developer.android.com/studio/profile/apk-profiler 724 | --------------------------------------------------------------------------------