diff --git a/.depcheckrc.yml b/.depcheckrc.yml index 7f1d161c6bd..cf36961c27e 100644 --- a/.depcheckrc.yml +++ b/.depcheckrc.yml @@ -123,3 +123,6 @@ ignores: - '@types/react-test-renderer' # runtime for dependecies using Nitro Modules (@metamask/native-utils) - 'react-native-nitro-modules' + + # Used in Yarn plugin for preview builds + - '@yarnpkg/core' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9663c7a2fe6..b118ed8a225 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -9,6 +9,7 @@ tailwind.config.js @MetaMask/design-system-engineers .github/ @MetaMask/mobile-platform .github/CODEOWNERS @MetaMask/mobile-platform patches/ @MetaMask/mobile-platform +.github/builds.yml @MetaMask/mobile-platform app/core/Analytics/index.ts @MetaMask/mobile-platform app/core/Analytics/MetaMetrics.constants.ts @MetaMask/mobile-platform app/core/Analytics/MetaMetrics.test.ts @MetaMask/mobile-platform @@ -247,6 +248,8 @@ tests/framework/ @MetaMask/qa e2e/pages/ @MetaMask/qa e2e/utils/ @MetaMask/qa e2e/viewHelper.ts @MetaMask/qa +# Note: Test builds (main-test, flask-test) in build/builds.yml are owned by QA team +# but the file itself is protected by mobile-platform for consistency # Co-owned by Swaps and Ramps teams app/util/parseAmount.ts @MetaMask/swaps-engineers @MetaMask/ramp diff --git a/.github/MIGRATION.md b/.github/MIGRATION.md new file mode 100644 index 00000000000..a2143de98e2 --- /dev/null +++ b/.github/MIGRATION.md @@ -0,0 +1,545 @@ +# Build System Migration: Bitrise → GitHub Actions + +This document outlines the migration path from Bitrise to GitHub Actions using the centralized `builds.yml` configuration. + +## Migration Phases + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Phase 1: builds.yml for config ✅ COMPLETE │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Phase 1.5: Parallel validation in Bitrise 📍 NEXT │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Phase 2: Remove env remapping from build.sh ⏳ PENDING │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Phase 3: Add store deployment workflows ⏳ PENDING │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ Phase 4: Deprecate Bitrise ⏳ PENDING │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +> **Note:** E2E test workflows already exist in GitHub Actions (see `.github/workflows/run-e2e-*.yml`), so no migration needed for E2E. + +--- + +## Phase 1: builds.yml Configuration ✅ + +**Status:** Complete + +**What was done:** + +- Created `.github/builds.yml` as single source of truth +- Created `scripts/apply-build-config.js` to load and export config +- Created `scripts/validate-build-config.js` for CI validation +- Created `scripts/set-secrets-from-config.js` for secret mapping +- Created `.github/workflows/build.yml` GitHub Actions workflow +- **Added `remote_feature_flags` section** for build-time defaults of LaunchDarkly flags +- **Updated `RemoteFeatureFlagController`** to seed defaults from `REMOTE_FEATURE_FLAG_DEFAULTS` +- **Refactored selectors** (Perps, Earn) to use build-time defaults instead of hardcoded fallbacks + +**Files:** + +``` +.github/ +├── builds.yml # Build configuration (env vars, secrets, code fencing, remote_feature_flags) +├── builds.README.md # Architecture documentation +└── workflows/ + └── build.yml # GitHub Actions workflow + +scripts/ +├── apply-build-config.js # Loads config, exports env vars + REMOTE_FEATURE_FLAG_DEFAULTS +├── validate-build-config.js # Validates config structure +└── set-secrets-from-config.js # Maps GitHub Secrets → env vars + +app/core/Engine/controllers/ +└── remote-feature-flag-controller-init.ts # Seeds build-time defaults + +app/components/UI/Perps/selectors/featureFlags/ +└── index.ts # Updated to use build-time defaults + +app/components/UI/Earn/selectors/featureFlags/ +└── index.ts # Updated to use build-time defaults +``` + +### Remote Feature Flags Architecture + +**Pattern:** Single anchor with production defaults, override in dev/exp builds (same as `_servers`). + +```yaml +# builds.yml +_remote_feature_flags: &remote_feature_flags # Single anchor with prod defaults + perpsPerpTradingEnabled: false + earnPooledStakingEnabled: true + +builds: + main-prod: + remote_feature_flags: *remote_feature_flags # Use defaults + main-dev: + remote_feature_flags: + <<: *remote_feature_flags + perpsPerpTradingEnabled: true # Override for dev +``` + +``` +Flow: +───────────────────────────────────────────────────────────────────────── +builds.yml (build time) → REMOTE_FEATURE_FLAG_DEFAULTS (JSON env var) + → RemoteFeatureFlagController seeds defaults + → LaunchDarkly OVERRIDES at runtime + → Selectors read from remoteFeatureFlags +``` + +**Benefits:** + +- Single source of truth for feature flag defaults (one anchor) +- Explicit overrides show exactly what differs from production +- Removed ~50+ hardcoded `process.env.MM_*` checks from selectors +- LaunchDarkly still works with version gating + +--- + +## Phase 1.5: Parallel Validation in Bitrise 📍 + +**Status:** Next + +**Goal:** Run both old (remapping functions) and new (builds.yml) configuration in parallel within Bitrise builds. Compare outputs to validate the new config produces identical results before trusting it. + +### Why This Phase? + +- **Zero risk** - Bitrise still uses old remapping for actual builds +- **Early detection** - Catches config mismatches before Phase 2 +- **Builds confidence** - Team sees "✅ Config matches" in every build +- **Audit trail** - Bitrise logs show comparison results + +### Implementation + +#### Step 1: Create verification script ✅ + +The script has been created at `scripts/verify-build-config.js`. It: + +- Automatically detects build name from `METAMASK_BUILD_TYPE` + `METAMASK_ENVIRONMENT` +- Compares env vars, secret mappings, code fencing, and remote feature flags +- Supports `--strict` mode (exit with error on mismatch) and `--verbose` mode + +```bash +# Test locally +METAMASK_BUILD_TYPE=main METAMASK_ENVIRONMENT=production node scripts/verify-build-config.js --verbose + +# Output shows what matches and what differs from builds.yml +``` + +#### Step 2: Add to Bitrise workflows + +In `bitrise.yml`, add a verification step after the existing remapping but before the actual build. + +The script auto-detects the build name from `METAMASK_BUILD_TYPE` and `METAMASK_ENVIRONMENT`: + +```yaml +# After existing remapping runs (sets env vars the old way) +# Add this step BEFORE the actual build step +- script@1: + title: Verify builds.yml config matches + inputs: + - content: | + #!/bin/bash + # Don't use set -e initially - we want to control exit behavior + + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Phase 1.5: Parallel Validation ║" + echo "║ Comparing Bitrise env vars with builds.yml config ║" + echo "╚════════════════════════════════════════════════════════════╝" + + # Run verification (auto-detects build from METAMASK_BUILD_TYPE + METAMASK_ENVIRONMENT) + # Week 1-2: Run without --strict (warnings only) + # Week 3+: Add --strict to fail builds on mismatch + node scripts/verify-build-config.js --verbose + + # Uncomment below when ready for strict mode: + # node scripts/verify-build-config.js --strict +``` + +**Where to add this step:** + +- Find workflows that call `remapMainProdEnvVariables`, `remapMainDevEnvVariables`, etc. +- Add the verification step AFTER the remapping script, BEFORE `generateIosBinary` or `generateAndroidBinary` + +#### Step 3: Rollout strategy + +``` +Week 1: Add verification step with warnings only (don't fail builds) + - Change process.exit(1) to process.exit(0) temporarily + - Monitor logs for mismatches + +Week 2: Fix any mismatches found in builds.yml + +Week 3: Enable strict mode (fail on mismatch) + - Restore process.exit(1) + +Week 4: If all builds pass validation for 1 week, proceed to Phase 2 +``` + +### Success Criteria + +- [x] `verify-build-config.js` created and tested locally +- [x] Verification step added to Bitrise build workflows: + - `_android_build_template` (main Android builds) + - `_ios_build_template` (main iOS builds) + - `ios_e2e_build` (iOS E2E builds) + - `_android_e2e_build_template` (Android E2E builds) +- [ ] 1+ week of builds passing with "✅ Config verification PASSED" +- [ ] No mismatches detected in any build variant +- [ ] Team confident to proceed to Phase 2 + +### What Gets Validated + +**Environment Variables:** +| Variable | Why Critical | +| ------------------------- | ------------------------------------------- | +| `METAMASK_ENVIRONMENT` | Build identity - wrong = undefined behavior | +| `METAMASK_BUILD_TYPE` | Build identity - wrong = undefined behavior | +| `PORTFOLIO_API_URL` | API endpoint - wrong = API errors | +| `SECURITY_ALERTS_API_URL` | Security features - wrong = alerts broken | +| `RAMPS_ENVIRONMENT` | Ramps feature - wrong = payment issues | +| `IS_TEST` | Test mode flags - wrong = unexpected state | + +**Secret Mappings:** +| Secret | Why Critical | +| ------------------------------ | ------------------------------------------------- | +| `SEGMENT_WRITE_KEY` | Analytics - wrong mapping = data in wrong project | +| `MM_SENTRY_DSN` | Error tracking - wrong mapping = errors lost | +| `IOS_GOOGLE_CLIENT_ID` | Auth - wrong mapping = login fails | +| `MM_CARD_BAANX_API_CLIENT_KEY` | Card feature - wrong mapping = API errors | + +**Also Validated:** + +- Code fencing features (what code is included/excluded) +- Remote feature flag defaults (build-time LaunchDarkly defaults) + +--- + +## Phase 2: Remove Env Remapping from build.sh ⏳ + +**Status:** Pending (after Phase 1.5 validation succeeds) + +**Goal:** Replace 300+ lines of `remapXxxEnvVariables()` functions in `build.sh` with a single config load from `builds.yml`. + +### Current State (build.sh) + +```bash +# ~300 lines of remapping functions +remapMainDevEnvVariables() { ... } +remapMainProdEnvVariables() { ... } +remapMainBetaEnvVariables() { ... } +remapMainReleaseCandidateEnvVariables() { ... } +remapMainExperimentalEnvVariables() { ... } +remapMainTestEnvVariables() { ... } +remapMainE2EEnvVariables() { ... } +remapFlaskProdEnvVariables() { ... } +remapFlaskTestEnvVariables() { ... } +remapFlaskE2EEnvVariables() { ... } +remapEnvVariableQA() { ... } + +# Complex switch/case to call the right function +if [ "$METAMASK_BUILD_TYPE" == "main" ]; then + if [ "$METAMASK_ENVIRONMENT" == "production" ]; then + remapMainProdEnvVariables + elif [ "$METAMASK_ENVIRONMENT" == "beta" ]; then + remapMainBetaEnvVariables + # ... 20+ more conditions +``` + +### Target State (build.sh) + +```bash +#!/bin/bash +set -o pipefail + +PLATFORM=$1 +BUILD_NAME=$2 # e.g., "main-prod", "flask-dev" + +# ───────────────────────────────────────────────────────────────────────────── +# Validate inputs +# ───────────────────────────────────────────────────────────────────────────── +if [ -z "$PLATFORM" ] || [ -z "$BUILD_NAME" ]; then + echo "❌ Usage: ./scripts/build.sh " + echo "" + echo " Platforms: android, ios, expo-update, watcher" + echo "" + echo " Build names:" + echo " main-prod Main production build" + echo " main-rc Main release candidate" + echo " main-dev Main development" + echo " main-test Main E2E/test build" + echo " flask-prod Flask production build" + echo " flask-rc Flask release candidate" + echo " flask-dev Flask development" + echo " flask-test Flask E2E/test build" + echo "" + echo " Examples:" + echo " ./scripts/build.sh android main-prod" + echo " ./scripts/build.sh ios flask-dev" + exit 1 +fi + +# ───────────────────────────────────────────────────────────────────────────── +# Load config from builds.yml (REQUIRED - no fallback) +# ───────────────────────────────────────────────────────────────────────────── +echo "" +echo "📦 Loading configuration for '${BUILD_NAME}'..." +echo "" + +CONFIG_OUTPUT=$(node scripts/apply-build-config.js "${BUILD_NAME}" --export 2>&1) +CONFIG_EXIT_CODE=$? + +if [ $CONFIG_EXIT_CODE -ne 0 ]; then + echo "❌ Failed to load build configuration" + echo "" + echo "Error: ${CONFIG_OUTPUT}" + echo "" + echo "Run 'node scripts/validate-build-config.js' to check config validity." + exit 1 +fi + +# Apply the configuration +eval "$CONFIG_OUTPUT" + +echo "✅ Configuration loaded" +echo " METAMASK_BUILD_TYPE: ${METAMASK_BUILD_TYPE}" +echo " METAMASK_ENVIRONMENT: ${METAMASK_ENVIRONMENT}" +echo "" + +# ───────────────────────────────────────────────────────────────────────────── +# Platform-specific builds (unchanged logic) +# ───────────────────────────────────────────────────────────────────────────── +# ... rest of build.sh (prebuild_ios, prebuild_android, generateIosBinary, etc.) +``` + +### Migration Steps + +#### Step 2.1: Update build.sh to accept new argument format + +**Before:** + +```bash +./scripts/build.sh android main production +# ^platform ^mode ^environment (3 args) +``` + +**After:** + +```bash +./scripts/build.sh android main-prod +# ^platform ^build-name (2 args) +``` + +#### Step 2.2: Add config loading to build.sh + +Add the strict config loading block at the top of `build.sh` (after parameter parsing). + +#### Step 2.3: Update package.json scripts + +**Before:** + +```json +{ + "build:android:main:prod": "./scripts/build.sh android main production", + "build:android:main:dev": "./scripts/build.sh android main dev", + "build:ios:main:prod": "./scripts/build.sh ios main production", + "build:ios:flask:prod": "./scripts/build.sh ios flask production" +} +``` + +**After:** + +```json +{ + "build:android:main-prod": "./scripts/build.sh android main-prod", + "build:android:main-dev": "./scripts/build.sh android main-dev", + "build:ios:main-prod": "./scripts/build.sh ios main-prod", + "build:ios:flask-prod": "./scripts/build.sh ios flask-prod" +} +``` + +#### Step 2.4: Delete remapping functions from build.sh + +Remove these functions (~300 lines): + +- `remapEnvVariable()` +- `remapMainDevEnvVariables()` +- `remapMainProdEnvVariables()` +- `remapMainBetaEnvVariables()` +- `remapMainReleaseCandidateEnvVariables()` +- `remapMainExperimentalEnvVariables()` +- `remapMainTestEnvVariables()` +- `remapMainE2EEnvVariables()` +- `remapFlaskProdEnvVariables()` +- `remapFlaskTestEnvVariables()` +- `remapFlaskE2EEnvVariables()` +- `remapEnvVariableQA()` + +And the switch/case logic that calls them (lines 814-846 in current build.sh). + +#### Step 2.5: Test all build variants + +```bash +# Test each build variant +./scripts/build.sh android main-prod +./scripts/build.sh android main-dev +./scripts/build.sh android main-test +./scripts/build.sh android flask-prod +./scripts/build.sh ios main-prod +./scripts/build.sh ios main-dev +./scripts/build.sh ios flask-prod + +# Verify error handling +./scripts/build.sh android invalid-build # Should fail with clear error +./scripts/build.sh # Should show usage +``` + +### Backward Compatibility + +During migration, support both formats temporarily: + +```bash +# In build.sh, detect old vs new format +if [ -n "$3" ]; then + # Old format: ./scripts/build.sh android main production + BUILD_NAME="${2}-${3}" + echo "⚠️ Deprecated: Use './scripts/build.sh $1 ${BUILD_NAME}' instead" +else + # New format: ./scripts/build.sh android main-prod + BUILD_NAME="$2" +fi +``` + +--- + +## Phase 3: Store Deployment Workflows ⏳ + +**Goal:** Replace Bitrise store deployment pipelines with GitHub Actions. + +### Workflows to Create + +``` +.github/workflows/ +├── build.yml # ✅ Exists +├── deploy-android-play.yml # ⏳ Deploy to Play Store +├── deploy-ios-testflight.yml # ⏳ Deploy to TestFlight +├── deploy-ios-appstore.yml # ⏳ Deploy to App Store +└── release.yml # ⏳ Full release pipeline +``` + +### Key Components + +| Bitrise Step | GitHub Actions Equivalent | +| --------------------------------- | ------------------------------------------ | +| `deploy-to-bitrise-io` | `actions/upload-artifact@v4` | +| `google-play-deploy` | `r0adkll/upload-google-play@v1` | +| `deploy-to-itunesconnect-deliver` | `apple-actions/upload-testflight-build@v1` | +| `fastlane` | `ruby/setup-ruby` + `fastlane` | + +### Example: Play Store Deployment + +```yaml +# .github/workflows/deploy-android-play.yml +name: Deploy Android to Play Store + +on: + workflow_call: + inputs: + build_name: + required: true + type: string + track: + required: true + type: string # internal, alpha, beta, production + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Download APK artifact + uses: actions/download-artifact@v4 + with: + name: android-${{ inputs.build_name }} + + - name: Upload to Play Store + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }} + packageName: io.metamask + releaseFiles: app-prod-release.aab + track: ${{ inputs.track }} +``` + +--- + +## Phase 4: Deprecate Bitrise ⏳ + +**Goal:** Fully transition to GitHub Actions and remove Bitrise. + +### Checklist + +- [ ] All build variants working in GitHub Actions +- [ ] Store deployments working +- [x] E2E tests running in GitHub Actions (already complete) +- [ ] Nightly builds configured +- [ ] Team trained on new workflows +- [ ] Monitoring/alerting set up +- [ ] Documentation updated +- [ ] `bitrise.yml` removed from repository + +### Deprecation Timeline + +``` +Week 1-2: Run both systems in parallel (builds) +Week 3-4: Run both systems in parallel (deployments) +Week 5: Disable Bitrise triggers, monitor GitHub Actions +Week 6: Remove bitrise.yml, update documentation +``` + +--- + +## Quick Reference + +### Running Builds + +```bash +# Local development +./scripts/build.sh android main-dev +./scripts/build.sh ios flask-dev + +# CI (GitHub Actions) +# Triggered via workflow_dispatch or workflow_call +``` + +### Validating Configuration + +```bash +# Check config is valid +node scripts/validate-build-config.js + +# Preview env vars for a build +node scripts/apply-build-config.js main-prod --export +``` + +### Adding a New Build Variant + +1. Add to `.github/builds.yml` +2. Add to workflow dropdown in `.github/workflows/build.yml` +3. Add package.json script (optional) +4. Test: `./scripts/build.sh ` + +--- + +## Rollback Plan + +If issues arise during migration: + +1. **Phase 2 rollback:** Revert build.sh changes, keep old remapping functions +2. **Phase 3 rollback:** Keep Bitrise pipelines active, disable GitHub Actions deployment workflows +3. **Phase 4 rollback:** Re-enable Bitrise from git history + +All phases are designed to be reversible with minimal impact. diff --git a/.github/builds.README.md b/.github/builds.README.md new file mode 100644 index 00000000000..4f8ea770ef2 --- /dev/null +++ b/.github/builds.README.md @@ -0,0 +1,601 @@ +# MetaMask Mobile Build Architecture + +## Quick Reference + +``` +.github/ +├── builds.yml # WHAT to build (configuration data) +├── builds.README.md # This documentation +└── workflows/ + └── build.yml # HOW to build (CI/CD automation) + +scripts/ +├── apply-build-config.js # Reads builds.yml → sets env vars + remote flag defaults +├── validate-build-config.js # Validates builds.yml structure +└── set-secrets-from-config.js # Maps GitHub Secrets → env vars +``` + +--- + +## File Responsibilities + +### 1. `.github/builds.yml` — Configuration Data + +**Purpose:** Single source of truth for all build variants. + +**Contains:** + +- Environment variables (API URLs, build config) +- Secret mappings (which GitHub Secret to use for each env var) +- Code fencing features (what code to include/exclude) +- **Remote feature flag defaults** (build-time defaults for LaunchDarkly flags) + +**Example entry:** + +```yaml +builds: + main-prod: + github_environment: build-production + + env: + METAMASK_ENVIRONMENT: 'production' + METAMASK_BUILD_TYPE: 'main' + PORTFOLIO_API_URL: 'https://portfolio.api.cx.metamask.io' + + secrets: + SEGMENT_WRITE_KEY: 'SEGMENT_WRITE_KEY' + MM_SENTRY_DSN: 'MM_SENTRY_DSN' + + code_fencing: + - preinstalled-snaps + - keyring-snaps + + remote_feature_flags: + perpsPerpTradingEnabled: false + earnPooledStakingEnabled: true + earnStablecoinLendingEnabled: true +``` + +--- + +### 2. `.github/workflows/build.yml` — CI/CD Workflow + +**Purpose:** GitHub Actions workflow that orchestrates the build process. + +**Responsibilities:** + +1. Receive build request (manual trigger or called by another workflow) +2. Load configuration from `builds.yml` +3. Handle approval gates for production builds +4. Execute the build on appropriate runners + +**Flow:** + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ workflow_dispatch / workflow_call │ +│ (build_name, platform) │ +└─────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ JOB: prepare │ +│ ├── yarn install │ +│ ├── validate-build-config.js (fail fast if invalid) │ +│ └── Load config from builds.yml │ +│ └── Output: requires_approval, github_environment, secrets │ +└─────────────────────────────────────────────────────────────────┘ + │ + ┌─────────────┴─────────────┐ + │ │ + requires_approval=true requires_approval=false + │ │ + ▼ │ +┌───────────────────────────────┐ │ +│ JOB: approval │ │ +│ └── Wait for manual approval │ │ +└───────────────────────────────┘ │ + │ │ + └─────────────┬─────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ JOB: build (matrix: android/ios) │ +│ ├── yarn install │ +│ ├── apply-build-config.js --export (set env vars) │ +│ ├── set-secrets-from-config.js (map secrets) │ +│ └── ./scripts/build.sh │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +### 3. `scripts/apply-build-config.js` — Config Loader + +**Purpose:** Reads `builds.yml` and exports environment variables + remote feature flag defaults. + +**Two modes:** + +```bash +# Mode 1: Direct (sets process.env in Node.js) +node scripts/apply-build-config.js main-prod + +# Mode 2: Shell export (outputs eval-able statements) +eval "$(node scripts/apply-build-config.js main-prod --export)" +``` + +**What it does:** + +``` +builds.yml Environment Variables +─────────────────────────────────────────────────────────────────────────── +env: → METAMASK_ENVIRONMENT=production + METAMASK_ENVIRONMENT METAMASK_BUILD_TYPE=main + METAMASK_BUILD_TYPE PORTFOLIO_API_URL=https://portfolio.api.cx.metamask.io + PORTFOLIO_API_URL ... + +code_fencing: → CODE_FENCING_FEATURES=["preinstalled-snaps",...] + - preinstalled-snaps + - keyring-snaps + +remote_feature_flags: → REMOTE_FEATURE_FLAG_DEFAULTS={"perpsPerpTradingEnabled":false,...} + perpsPerpTradingEnabled: false + earnPooledStakingEnabled: true +``` + +**Server URL Strategy:** + +- `_servers` anchor: Production URLs as defaults +- Test/e2e/exp/dev builds override with UAT/dev URLs +- GitHub Environment determines actual secret values (same secret names, different values per environment) + +--- + +### 4. `scripts/validate-build-config.js` — Config Validator + +**Purpose:** Validates `builds.yml` structure before builds run. + +**Checks:** + +- YAML syntax is valid +- All builds have required fields: + - `env.METAMASK_ENVIRONMENT` + - `env.METAMASK_BUILD_TYPE` + - `github_environment` + +**Usage:** + +```bash +node scripts/validate-build-config.js +# ✅ Valid: 8 builds configured +# main-prod, main-rc, main-dev, main-test, flask-prod, flask-rc, flask-dev, flask-test +``` + +--- + +### 5. `scripts/set-secrets-from-config.js` — Secret Mapper + +**Purpose:** Maps GitHub Secrets to environment variables based on config. + +**How it works:** + +``` +builds.yml secrets: GitHub Secrets App expects +───────────────────────────────────────────────────────────────────────────── +SEGMENT_WRITE_KEY: "SEGMENT_KEY_QA" → $SEGMENT_KEY_QA → $SEGMENT_WRITE_KEY +MM_SENTRY_DSN: "MM_SENTRY_DSN_TEST" → $MM_SENTRY_DSN_TEST → $MM_SENTRY_DSN +``` + +**Why mapping?** Different builds use different secrets, but the app always expects the same env var names. + +**Usage:** + +```bash +CONFIG_SECRETS='{"SEGMENT_WRITE_KEY":"SEGMENT_KEY_QA"}' node scripts/set-secrets-from-config.js +# ✓ SEGMENT_WRITE_KEY +``` + +--- + +## Data Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ BUILD TIME │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ .github/builds.yml │ +│ │ │ +│ ├────────────────┬────────────────┬────────────────┐ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │ +│ │ env │ │ secrets │ │ code_ │ │ remote_feature_ │ │ +│ │ │ │ │ │ fencing │ │ flags │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └───────┬─────────┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ apply-build- set-secrets- metro.transform.js apply-build- │ +│ config.js from-config.js (removes code) config.js │ +│ │ │ │ │ │ +│ └───────┬───────┘ │ │ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ Environment Vars Bundled JavaScript REMOTE_FEATURE_ │ +│ │ │ FLAG_DEFAULTS │ +│ │ │ │ │ +│ └───────────┬───────────┴──────────────────┘ │ +│ │ │ +│ ▼ │ +│ Built App (.apk / .ipa) │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────┐ +│ RUNTIME │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ RemoteFeatureFlagController │ +│ ├── Build-time defaults seeded (from REMOTE_FEATURE_FLAG_DEFAULTS) │ +│ │ │ +│ ├── LaunchDarkly fetches and OVERRIDES at runtime │ +│ │ │ +│ └── Selectors read from remoteFeatureFlags │ +│ │ │ +│ ▼ │ +│ App behavior (feature enabled/disabled) │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Priority Order + +When the same variable is set in multiple places: + +``` +1. .js.env (local dev only) ← Highest priority +2. LaunchDarkly (runtime) ← Overrides remote feature flags +3. builds.yml (build time) ← Default values +``` + +**Example for env vars:** + +```bash +# builds.yml sets: +RAMPS_ENVIRONMENT: "production" + +# Developer's .js.env overrides for local testing: +export RAMPS_ENVIRONMENT="staging" + +# Result: App uses "staging" locally, "production" in CI builds +``` + +--- + +## Remote Feature Flags + +`builds.yml` is the **single source of truth** for remote feature flag defaults. These are seeded into `RemoteFeatureFlagController` at startup, then LaunchDarkly can override them at runtime. + +### Pattern: Single Anchor with Overrides + +Following the same pattern as `_servers` and `_secrets`: + +```yaml +# Single anchor with production (conservative) defaults +_remote_feature_flags: &remote_feature_flags + perpsPerpTradingEnabled: false + earnPooledStakingEnabled: true + earnMusdConversionFlowEnabled: false + +builds: + # Prod/RC/Test/E2E use defaults directly + main-prod: + remote_feature_flags: *remote_feature_flags + + # Dev/Exp override specific flags + main-dev: + remote_feature_flags: + <<: *remote_feature_flags + perpsPerpTradingEnabled: true # Enable for testing + earnMusdConversionFlowEnabled: true +``` + +### How It Works + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ builds.yml │ +│ ├── _remote_feature_flags (anchor with prod defaults) │ +│ ├── main-prod uses *remote_feature_flags directly │ +│ └── main-dev uses <<: *remote_feature_flags + overrides │ +│ │ +│ ↓ (build time) │ +│ │ +│ REMOTE_FEATURE_FLAG_DEFAULTS env var (JSON) │ +│ │ +│ ↓ (app startup) │ +│ │ +│ RemoteFeatureFlagController seeds defaults │ +│ │ +│ ↓ (runtime) │ +│ │ +│ LaunchDarkly fetches and OVERRIDES (versioned flags with minAppVersion) │ +│ │ +│ ↓ │ +│ │ +│ Selectors read from remoteFeatureFlags (single source) │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Selector Pattern + +Selectors use this pattern to read feature flags: + +```typescript +export const selectPerpsEnabledFlag = createSelector( + selectRemoteFeatureFlags, + (remoteFeatureFlags) => { + const remoteFlag = + remoteFeatureFlags?.perpsPerpTradingEnabled as VersionGatedFeatureFlag; + + // Try versioned flag first (from LaunchDarkly), fall back to build-time default + return ( + validatedVersionGatedFeatureFlag(remoteFlag) ?? + (remoteFeatureFlags?.perpsPerpTradingEnabled as boolean) + ); + }, +); +``` + +**Flow:** + +1. `validatedVersionGatedFeatureFlag()` checks if LaunchDarkly returned a versioned flag with version gating +2. If not (returns `undefined`), falls back to the build-time default (simple boolean from `builds.yml`) + +### Benefits + +- **Single source of truth** - One anchor defines all production defaults +- **Explicit overrides** - Dev builds show exactly which flags differ from production +- **No hardcoded fallbacks** - Selectors trust the seeded defaults +- **Easy maintenance** - Adding a new flag only requires updating one anchor +- **LaunchDarkly still works** - Runtime overrides with version gating + +--- + +## Build Variants + +| Build | Environment | GitHub Environment | Use Case | +| ------------ | ----------- | ------------------ | ------------------------- | +| `main-prod` | production | build-production | App Store release | +| `main-rc` | rc | build-rc | Release candidate testing | +| `main-test` | test | build-test | QA testing | +| `main-e2e` | e2e | build-e2e | E2E automated tests | +| `main-exp` | exp | build-exp | Experimental builds | +| `main-dev` | dev | build-dev | Local development | +| `flask-prod` | production | build-production | Flask App Store release | +| `flask-test` | test | build-test | Flask QA testing | +| `flask-e2e` | e2e | build-e2e | Flask E2E tests | +| `flask-dev` | dev | build-dev | Flask local development | + +--- + +## YAML Anchors + +`builds.yml` uses YAML anchors to avoid repetition. The pattern is: **single anchor with production defaults, override in specific builds as needed**. + +```yaml +# Define anchors with production (conservative) defaults +_servers: &servers + PORTFOLIO_API_URL: 'https://portfolio.api.cx.metamask.io' + SECURITY_ALERTS_API_URL: 'https://security-alerts.api.cx.metamask.io' + +_remote_feature_flags: &remote_feature_flags + perpsPerpTradingEnabled: false + earnPooledStakingEnabled: true + earnMusdConversionFlowEnabled: false + +# Reuse via aliases, override as needed +builds: + main-prod: + env: + <<: *servers + remote_feature_flags: *remote_feature_flags # Use defaults directly + + main-dev: + env: + <<: *servers + PORTFOLIO_API_URL: 'https://portfolio.dev-api.cx.metamask.io' # Override URL + remote_feature_flags: + <<: *remote_feature_flags + # Override specific flags for dev testing + perpsPerpTradingEnabled: true + earnMusdConversionFlowEnabled: true +``` + +**Key:** Anchors are resolved at YAML parse time, not runtime. No magic inheritance logic needed. + +**Pattern Benefits:** + +- Single source of truth for defaults +- Explicit overrides show exactly what differs from production +- Adding a new flag only requires updating one anchor + +--- + +## How to Add Things + +### New Remote Feature Flag (Recommended) + +For feature flags that LaunchDarkly may control, add to `remote_feature_flags`: + +1. Add to `.github/builds.yml` anchor (production default): + +```yaml +# Add to the single anchor with production (conservative) default +_remote_feature_flags: &remote_feature_flags # ... existing flags ... + myNewFeatureEnabled: false # Conservative for prod +``` + +2. Override in dev/exp builds if needed: + +```yaml +builds: + main-dev: + remote_feature_flags: + <<: *remote_feature_flags + # Override for dev testing + myNewFeatureEnabled: true +``` + +3. Create selector in your feature's selectors: + +```typescript +export const selectMyNewFeatureEnabled = createSelector( + selectRemoteFeatureFlags, + (remoteFeatureFlags) => { + const remoteFlag = + remoteFeatureFlags?.myNewFeatureEnabled as VersionGatedFeatureFlag; + return ( + validatedVersionGatedFeatureFlag(remoteFlag) ?? + (remoteFeatureFlags?.myNewFeatureEnabled as boolean) + ); + }, +); +``` + +4. Use in component: + +```typescript +const isEnabled = useSelector(selectMyNewFeatureEnabled); +``` + +### New Environment Variable (Build Config) + +For non-feature-flag config (API URLs, build settings), add to `env`: + +1. Add to `.github/builds.yml`: + +```yaml +builds: + main-prod: + env: + MY_API_URL: 'https://api.example.com' + main-dev: + env: + MY_API_URL: 'https://dev-api.example.com' +``` + +2. Document in `.js.env.example`: + +```bash +export MY_API_URL="https://api.example.com" +``` + +3. Use in code: + +```typescript +const apiUrl = process.env.MY_API_URL; +``` + +### New Secret + +1. Add to GitHub repository secrets + +2. Add mapping in `.github/builds.yml`: + +```yaml +builds: + main-prod: + secrets: + MY_API_KEY: 'MY_API_KEY_PROD' + main-dev: + secrets: + MY_API_KEY: 'MY_API_KEY_DEV' +``` + +### New Build Variant + +1. Add to `.github/builds.yml`: + +```yaml +builds: + main-beta: + requires_approval: true + github_environment: build-beta + env: + METAMASK_ENVIRONMENT: 'beta' + METAMASK_BUILD_TYPE: 'main' + # ... rest of config +``` + +2. Add to workflow dropdown in `.github/workflows/build.yml`: + +```yaml +build_name: + type: choice + options: + - main-prod + - main-beta # Add here + - main-rc +``` + +--- + +## Troubleshooting + +### Build not found + +``` +❌ Build "main-foo" not found. Available: main-prod, main-rc, ... +``` + +→ Check build name matches exactly in `.github/builds.yml` + +### Validation failed + +``` +❌ main-prod: missing env.METAMASK_ENVIRONMENT +``` + +→ Ensure all required fields are present + +### Remote feature flag not working + +1. **Check builds.yml** - Is the flag defined in `remote_feature_flags`? +2. **Check LaunchDarkly** - Is LaunchDarkly overriding with a different value? +3. **Check selector** - Does it follow the pattern? + ```typescript + validatedVersionGatedFeatureFlag(remoteFlag) ?? + (remoteFeatureFlags?.flagName as boolean); + ``` +4. **Check flag name** - LaunchDarkly flag name must match exactly (e.g., `perpsPerpTradingEnabled`) + +### Secrets not set + +``` +⚠ SEGMENT_KEY_PROD not found (for SEGMENT_WRITE_KEY) +``` + +→ Ensure GitHub Secret exists with exact name from `secrets` mapping + +--- + +## Testing Locally + +```bash +# Validate config +node scripts/validate-build-config.js + +# See what env vars would be set (including remote feature flag defaults) +node scripts/apply-build-config.js main-dev --export + +# Apply config and verify +eval "$(node scripts/apply-build-config.js main-dev --export)" +echo $METAMASK_ENVIRONMENT # Should print: dev +echo $REMOTE_FEATURE_FLAG_DEFAULTS # Should print: {"perpsPerpTradingEnabled":true,...} + +# Parse remote feature flags JSON +node -e "console.log(JSON.parse(process.env.REMOTE_FEATURE_FLAG_DEFAULTS))" +``` diff --git a/.github/builds.yml b/.github/builds.yml new file mode 100644 index 00000000000..de2208fd0f7 --- /dev/null +++ b/.github/builds.yml @@ -0,0 +1,351 @@ +# Single source of truth for all build configurations +# Protected by CODEOWNERS: mobile-platform team +# +# Usage: +# - CI reads this file to configure builds +# - Local dev can override via .js.env (takes precedence) +# - LaunchDarkly overrides remote_feature_flags at runtime +# +# Structure: +# - env: sets environment variables directly (servers, build config, etc.) +# - secrets: maps env var names to GitHub Secret names +# - code_fencing: features to include via code fencing +# - remote_feature_flags: build-time defaults for LaunchDarkly flags (seeded into RemoteFeatureFlagController) +# +# GitHub Environment determines actual secret values - builds.yml just maps the names. + +# ============================================================================= +# YAML Anchors (reusable config blocks) +# ============================================================================= + +# Server URLs (production defaults, override in test/e2e/exp/dev builds) +_servers: &servers + PORTFOLIO_API_URL: "https://portfolio.api.cx.metamask.io" + SECURITY_ALERTS_API_URL: "https://security-alerts.api.cx.metamask.io" + DECODING_API_URL: "https://signature-insights.api.cx.metamask.io/v1" + AUTH_SERVICE_URL: "https://auth-service.api.cx.metamask.io" + REWARDS_API_URL: "https://rewards.api.cx.metamask.io" + BAANX_API_URL: "https://api.baanx.com" + RAMPS_ENVIRONMENT: "production" + +# Common secrets (shared across ALL builds - same names, GitHub Environment determines values) +_secrets: &secrets + # Infrastructure + MM_SENTRY_AUTH_TOKEN: "MM_SENTRY_AUTH_TOKEN" + MM_SENTRY_DSN: "MM_SENTRY_DSN" + GOOGLE_SERVICES_B64_IOS: "GOOGLE_SERVICES_B64_IOS" + GOOGLE_SERVICES_B64_ANDROID: "GOOGLE_SERVICES_B64_ANDROID" + # API Keys + MM_INFURA_PROJECT_ID: "MM_INFURA_PROJECT_ID" + WALLET_CONNECT_PROJECT_ID: "WALLET_CONNECT_PROJECT_ID" + MM_FOX_CODE: "MM_FOX_CODE" + MM_BRANCH_KEY_LIVE: "MM_BRANCH_KEY_LIVE" + # Analytics + SEGMENT_WRITE_KEY: "SEGMENT_WRITE_KEY" + SEGMENT_PROXY_URL: "SEGMENT_PROXY_URL" + SEGMENT_DELETE_API_SOURCE_ID: "SEGMENT_DELETE_API_SOURCE_ID" + SEGMENT_REGULATIONS_ENDPOINT: "SEGMENT_REGULATIONS_ENDPOINT" + # Features/Announcements + FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN: "FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN" + FEATURES_ANNOUNCEMENTS_SPACE_ID: "FEATURES_ANNOUNCEMENTS_SPACE_ID" + # Firebase + FCM_CONFIG_API_KEY: "FCM_CONFIG_API_KEY" + FCM_CONFIG_AUTH_DOMAIN: "FCM_CONFIG_AUTH_DOMAIN" + FCM_CONFIG_STORAGE_BUCKET: "FCM_CONFIG_STORAGE_BUCKET" + FCM_CONFIG_PROJECT_ID: "FCM_CONFIG_PROJECT_ID" + FCM_CONFIG_MESSAGING_SENDER_ID: "FCM_CONFIG_MESSAGING_SENDER_ID" + FCM_CONFIG_APP_ID: "FCM_CONFIG_APP_ID" + FCM_CONFIG_MEASUREMENT_ID: "FCM_CONFIG_MEASUREMENT_ID" + # QuickNode + QUICKNODE_MAINNET_URL: "QUICKNODE_MAINNET_URL" + QUICKNODE_ARBITRUM_URL: "QUICKNODE_ARBITRUM_URL" + QUICKNODE_AVALANCHE_URL: "QUICKNODE_AVALANCHE_URL" + QUICKNODE_BASE_URL: "QUICKNODE_BASE_URL" + QUICKNODE_LINEA_MAINNET_URL: "QUICKNODE_LINEA_MAINNET_URL" + QUICKNODE_MONAD_URL: "QUICKNODE_MONAD_URL" + QUICKNODE_OPTIMISM_URL: "QUICKNODE_OPTIMISM_URL" + QUICKNODE_POLYGON_URL: "QUICKNODE_POLYGON_URL" + # OAuth + IOS_GOOGLE_CLIENT_ID: "IOS_GOOGLE_CLIENT_ID" + IOS_GOOGLE_REDIRECT_URI: "IOS_GOOGLE_REDIRECT_URI" + ANDROID_APPLE_CLIENT_ID: "ANDROID_APPLE_CLIENT_ID" + ANDROID_GOOGLE_CLIENT_ID: "ANDROID_GOOGLE_CLIENT_ID" + ANDROID_GOOGLE_SERVER_CLIENT_ID: "ANDROID_GOOGLE_SERVER_CLIENT_ID" + # Card/Baanx + MM_CARD_BAANX_API_CLIENT_KEY: "MM_CARD_BAANX_API_CLIENT_KEY" + +# Main code fencing features +_code_fencing_main: &code_fencing_main + - preinstalled-snaps + - keyring-snaps + - multi-srp + - solana + - bitcoin + - tron + +# Flask code fencing features (includes experimental) +_code_fencing_flask: &code_fencing_flask + - flask + - preinstalled-snaps + - external-snaps + - keyring-snaps + - multi-srp + - solana + - bitcoin + - tron + +# ============================================================================= +# Remote Feature Flags - Build-time defaults (seeded into RemoteFeatureFlagController) +# LaunchDarkly will override these at runtime +# Production (conservative) defaults - override in dev/exp builds as needed +# ============================================================================= +_remote_feature_flags: &remote_feature_flags + # Perps + perpsPerpTradingEnabled: false + perpsPerpTradingServiceInterruptionBannerEnabled: false + perpsPerpGtmOnboardingModalEnabled: false + perpsOrderBookEnabled: false + perpsFeedbackEnabled: false + # Earn/Staking + earnPooledStakingEnabled: true + earnPooledStakingServiceInterruptionBannerEnabled: false + earnStablecoinLendingEnabled: true + earnStablecoinLendingServiceInterruptionBannerEnabled: false + earnMusdConversionFlowEnabled: false + earnMusdCtaEnabled: false + earnMusdConversionAssetOverviewCtaEnabled: false + earnMusdConversionTokenListItemCtaEnabled: false + earnMusdConversionRewardsUiEnabled: false + earnMerklCampaignClaiming: false + +# ============================================================================= +# Build Configurations +# ============================================================================= +# Based on client distribution matrix - see docs for full mapping +# Environments: production, rc, test, e2e, exp, dev + +builds: + # --------------------------------------------------------------------------- + # MAIN BUILDS + # --------------------------------------------------------------------------- + + # Production release (App Store) + main-prod: + github_environment: build-production + env: + METAMASK_ENVIRONMENT: "production" + METAMASK_BUILD_TYPE: "main" + <<: *servers + # Build config flags (not remote feature flags) + BRIDGE_USE_DEV_APIS: "false" + RAMP_INTERNAL_BUILD: "false" + IS_TEST: "false" + secrets: *secrets + code_fencing: *code_fencing_main + remote_feature_flags: *remote_feature_flags + + # Release candidate + main-rc: + github_environment: build-rc + env: + METAMASK_ENVIRONMENT: "rc" + METAMASK_BUILD_TYPE: "main" + <<: *servers + BRIDGE_USE_DEV_APIS: "false" + RAMP_INTERNAL_BUILD: "false" + IS_TEST: "false" + secrets: *secrets + code_fencing: *code_fencing_main + remote_feature_flags: *remote_feature_flags + + # Test builds (QA) + main-test: + github_environment: build-test + env: + METAMASK_ENVIRONMENT: "test" + METAMASK_BUILD_TYPE: "main" + <<: *servers + # UAT server overrides (TODO: Add actual UAT URLs when available) + PORTFOLIO_API_URL: "https://portfolio.dev-api.cx.metamask.io" + REWARDS_API_URL: "https://rewards.uat-api.cx.metamask.io" + BAANX_API_URL: "https://dev.api.baanx.com" + RAMPS_ENVIRONMENT: "staging" + BRIDGE_USE_DEV_APIS: "true" + RAMP_INTERNAL_BUILD: "true" + IS_TEST: "true" + IGNORE_BOXLOGS_DEVELOPMENT: "true" + IS_SIM_BUILD: "true" + secrets: *secrets + code_fencing: *code_fencing_main + remote_feature_flags: *remote_feature_flags + + # E2E test builds (no Sentry) + main-e2e: + github_environment: build-e2e + env: + METAMASK_ENVIRONMENT: "e2e" + METAMASK_BUILD_TYPE: "main" + <<: *servers + PORTFOLIO_API_URL: "https://portfolio.dev-api.cx.metamask.io" + REWARDS_API_URL: "https://rewards.uat-api.cx.metamask.io" + BAANX_API_URL: "https://dev.api.baanx.com" + RAMPS_ENVIRONMENT: "staging" + BRIDGE_USE_DEV_APIS: "true" + RAMP_INTERNAL_BUILD: "true" + IS_TEST: "true" + IGNORE_BOXLOGS_DEVELOPMENT: "true" + IS_SIM_BUILD: "true" + secrets: *secrets + code_fencing: *code_fencing_main + remote_feature_flags: *remote_feature_flags + + # Experimental builds + main-exp: + github_environment: build-exp + env: + METAMASK_ENVIRONMENT: "exp" + METAMASK_BUILD_TYPE: "main" + <<: *servers + PORTFOLIO_API_URL: "https://portfolio.dev-api.cx.metamask.io" + REWARDS_API_URL: "https://rewards.uat-api.cx.metamask.io" + BAANX_API_URL: "https://dev.api.baanx.com" + RAMPS_ENVIRONMENT: "staging" + BRIDGE_USE_DEV_APIS: "true" + RAMP_INTERNAL_BUILD: "true" + IS_TEST: "false" + MM_ENABLE_SETTINGS_PAGE_DEV_OPTIONS: "true" + secrets: *secrets + code_fencing: *code_fencing_main + remote_feature_flags: + <<: *remote_feature_flags + # Override for experimental testing + perpsPerpTradingEnabled: true + perpsPerpGtmOnboardingModalEnabled: true + perpsOrderBookEnabled: true + perpsFeedbackEnabled: true + earnMusdConversionFlowEnabled: true + earnMusdCtaEnabled: true + earnMusdConversionAssetOverviewCtaEnabled: true + earnMusdConversionTokenListItemCtaEnabled: true + earnMusdConversionRewardsUiEnabled: true + earnMerklCampaignClaiming: true + + # Local development (primarily for .js.env reference) + main-dev: + github_environment: build-dev + env: + METAMASK_ENVIRONMENT: "dev" + METAMASK_BUILD_TYPE: "main" + <<: *servers + PORTFOLIO_API_URL: "https://portfolio.dev-api.cx.metamask.io" + REWARDS_API_URL: "https://rewards.uat-api.cx.metamask.io" + BAANX_API_URL: "https://dev.api.baanx.com" + RAMPS_ENVIRONMENT: "staging" + BRIDGE_USE_DEV_APIS: "true" + RAMP_INTERNAL_BUILD: "true" + IS_TEST: "false" + MM_ENABLE_SETTINGS_PAGE_DEV_OPTIONS: "true" + secrets: *secrets + code_fencing: *code_fencing_main + remote_feature_flags: + <<: *remote_feature_flags + # Override for dev testing + perpsPerpTradingEnabled: true + perpsPerpGtmOnboardingModalEnabled: true + perpsOrderBookEnabled: true + perpsFeedbackEnabled: true + earnMusdConversionFlowEnabled: true + earnMusdCtaEnabled: true + earnMusdConversionAssetOverviewCtaEnabled: true + earnMusdConversionTokenListItemCtaEnabled: true + earnMusdConversionRewardsUiEnabled: true + earnMerklCampaignClaiming: true + + # --------------------------------------------------------------------------- + # FLASK BUILDS + # --------------------------------------------------------------------------- + + # Flask production release + flask-prod: + github_environment: build-production + env: + METAMASK_ENVIRONMENT: "production" + METAMASK_BUILD_TYPE: "flask" + <<: *servers + BRIDGE_USE_DEV_APIS: "false" + RAMP_INTERNAL_BUILD: "false" + IS_TEST: "false" + secrets: *secrets + code_fencing: *code_fencing_flask + remote_feature_flags: *remote_feature_flags + + # Flask test builds (QA) + flask-test: + github_environment: build-test + env: + METAMASK_ENVIRONMENT: "test" + METAMASK_BUILD_TYPE: "flask" + <<: *servers + PORTFOLIO_API_URL: "https://portfolio.dev-api.cx.metamask.io" + REWARDS_API_URL: "https://rewards.uat-api.cx.metamask.io" + BAANX_API_URL: "https://dev.api.baanx.com" + RAMPS_ENVIRONMENT: "staging" + BRIDGE_USE_DEV_APIS: "true" + RAMP_INTERNAL_BUILD: "true" + IS_TEST: "true" + IGNORE_BOXLOGS_DEVELOPMENT: "true" + IS_SIM_BUILD: "true" + secrets: *secrets + code_fencing: *code_fencing_flask + remote_feature_flags: *remote_feature_flags + + # Flask E2E test builds (no Sentry) + flask-e2e: + github_environment: build-e2e + env: + METAMASK_ENVIRONMENT: "e2e" + METAMASK_BUILD_TYPE: "flask" + <<: *servers + PORTFOLIO_API_URL: "https://portfolio.dev-api.cx.metamask.io" + REWARDS_API_URL: "https://rewards.uat-api.cx.metamask.io" + BAANX_API_URL: "https://dev.api.baanx.com" + RAMPS_ENVIRONMENT: "staging" + BRIDGE_USE_DEV_APIS: "true" + RAMP_INTERNAL_BUILD: "true" + IS_TEST: "true" + IGNORE_BOXLOGS_DEVELOPMENT: "true" + IS_SIM_BUILD: "true" + secrets: *secrets + code_fencing: *code_fencing_flask + remote_feature_flags: *remote_feature_flags + + # Flask local development (primarily for .js.env reference) + flask-dev: + github_environment: build-dev + env: + METAMASK_ENVIRONMENT: "dev" + METAMASK_BUILD_TYPE: "flask" + <<: *servers + PORTFOLIO_API_URL: "https://portfolio.dev-api.cx.metamask.io" + REWARDS_API_URL: "https://rewards.uat-api.cx.metamask.io" + BAANX_API_URL: "https://dev.api.baanx.com" + RAMPS_ENVIRONMENT: "staging" + BRIDGE_USE_DEV_APIS: "true" + RAMP_INTERNAL_BUILD: "true" + IS_TEST: "false" + MM_ENABLE_SETTINGS_PAGE_DEV_OPTIONS: "true" + secrets: *secrets + code_fencing: *code_fencing_flask + remote_feature_flags: + <<: *remote_feature_flags + # Override for dev testing + perpsPerpTradingEnabled: true + perpsPerpGtmOnboardingModalEnabled: true + perpsOrderBookEnabled: true + perpsFeedbackEnabled: true + earnMusdConversionFlowEnabled: true + earnMusdCtaEnabled: true + earnMusdConversionAssetOverviewCtaEnabled: true + earnMusdConversionTokenListItemCtaEnabled: true + earnMusdConversionRewardsUiEnabled: true + earnMerklCampaignClaiming: true diff --git a/.github/workflows/push-eas-update.yml b/.github/workflows/push-eas-update.yml index 719672ecb50..ab37ef9b9aa 100644 --- a/.github/workflows/push-eas-update.yml +++ b/.github/workflows/push-eas-update.yml @@ -28,6 +28,15 @@ on: - rc - production default: exp + platform: + description: 'Platform to update (all, ios, android)' + required: true + type: choice + options: + - all + - ios + - android + default: all permissions: contents: read @@ -39,6 +48,7 @@ env: BASE_BRANCH_REF: ${{ inputs.base_branch }} UPDATE_MESSAGE: ${{ inputs.message }} TARGET_CHANNEL: ${{ inputs.channel }} + OTA_PUSH_PLATFORM: ${{ inputs.platform }} jobs: setup-dependencies: @@ -403,8 +413,6 @@ jobs: SKIP_TRANSFORM_LINT: 'true' # Increase Node heap to avoid OOM during Expo export in CI NODE_OPTIONS: '--max_old_space_size=8192' - # Disable LavaMoat sandbox to prevent duplicate bundle executions in CI - EXPO_NO_LAVAMOAT: '1' run: | echo "📦 Configuring EXPO key..." if [[ -z "$EXPO_KEY_PRIV_B64" ]]; then diff --git a/.yarn/plugins/@yarnpkg/plugin-preview-builds.cjs b/.yarn/plugins/@yarnpkg/plugin-preview-builds.cjs new file mode 100644 index 00000000000..5175f5d294b --- /dev/null +++ b/.yarn/plugins/@yarnpkg/plugin-preview-builds.cjs @@ -0,0 +1,555 @@ +/** + * This plugin makes it easier for engineers to use preview builds (prereleases + * published under `@metamask-previews` or a custom NPM scope). It hooks into + * Yarn's resolution workflow to alias `@metamask` dependencies to specific + * versions of preview packages. + * + * It assumes that preview builds have been specified using a `previewBuilds` + * key in `package.json`, for example, this would alias whatever version of + * `@metamask/network-controller` is being used to + * `@metamask-previews/network-controller@29.0.0-preview-3ec2a74`. + * + * "previewBuilds": { + * "@metamask/network-controller": { + * "type": "non-breaking", + * "previewVersion": "29.0.0-preview-3ec2a74" + * } + * } + * + * If a dependency is patched, the patch is preserved. + * + * There are two ways to specify the `type` within a preview build + * configuration. If an engineer wants to test breaking changes, they would + * specify `"breaking"`; otherwise they would specify `"non-breaking"`. + * + * To use a custom scope instead of `@metamask-previews`, an optional + * `previewScope` is supported, e.g.: + * + * "previewBuilds": { + * "@metamask/network-controller": { + * "type": "non-breaking", + * "previewScope": "my-custom-scope" + * "previewVersion": "29.0.0-preview-3ec2a74", + * } + * } + */ + +/** + * @typedef {'breaking' | 'non-breaking'} PreviewBuildType + * + * The nature of the changes in the preview build. + + * For breaking changes, only the dependency at the top level will be resolved + * to the preview build. + * + * For non-breaking changes, all instances of the dependency that match the + * major part of the version range specified in `package.json` will be resolved + * to the preview build. + */ + +/** + * @typedef {object} PreviewBuildConfiguration + * @property {PreviewBuildType} type - The nature of the changes in the preview + * build. + * @property {string} previewScope - The scope for the preview build (defaults + * @property {string} previewVersion - The preview version string. + * to "metamask-previews"). + * @property {string} previewVersion - The preview version string. + */ + +/** + * @typedef {Record} PreviewBuildConfigurations + */ + +/** + * @typedef {object} ValidatedPreviewBuild + * @property {PreviewBuildConfiguration} configuration - The validated preview + * build configuration. + * @property {YarnDescriptor} originalDependencyDescriptor - The descriptor for + * the original dependency in `package.json`. + */ + +/** + * @typedef {import('@yarnpkg/core').Configuration} YarnConfiguration + */ + +/** + * @typedef {import('@yarnpkg/core').Descriptor} YarnDescriptor + */ + +/** + * @typedef {import('@yarnpkg/core').Locator} YarnLocator + */ + +/** + * @typedef {import('@yarnpkg/core').Project} YarnProject + */ + +module.exports = { + name: '@yarnpkg/plugin-preview-builds', + factory: (require) => { + const { structUtils } = require('@yarnpkg/core'); + + /** + * The scope that all MetaMask packages are published under. + */ + const METAMASK_NPM_SCOPE = 'metamask'; + + /** + * The default scope for preview builds. + */ + const DEFAULT_PREVIEW_SCOPE = 'metamask-previews'; + + /** + * Stores validated preview build configurations after `validateProject` runs. + * + * @type {Map} + */ + const validatedPreviewBuilds = new Map(); + + /** + * Stores resolutions for preview builds that have been created in the + * `reduceDependency` hook. Used to print a summary in `afterAllInstalled`. + * + * @type {Map} + */ + const resolutions = new Map(); + + /** + * Stores unique error messages to display after installation. + * + * @type {Set} + */ + const errors = new Set(); + + /** + * Extracts the major version from a SemVer-compatible version range. + * + * @param {string} semVerVersionRange - The version range (e.g., "^10.0.0", + * "10.2.3", ">=10.0.0"). + * @returns {number|null} The major version number, or `null` if it can't be + * determined. + */ + function getMajorVersion(semVerVersionRange) { + const cleaned = semVerVersionRange.replace(/^[\^~>=<]+/, ''); + const match = cleaned.match(/^(\d+)/); + return match ? parseInt(match[1], 10) : null; + } + + /** + * Checks if a Yarn descriptor represents a patched dependency. + * + * @param {string} rawDescriptorRange - The descriptor range string. + * @returns {boolean} True if the descriptor is a patch. + */ + function isPatchedDescriptor(rawDescriptorRange) { + return rawDescriptorRange.startsWith('patch:'); + } + + /** + * Formats a Yarn descriptor range or locator reference, colorizing it like + * Yarn would. + * + * @param {YarnConfiguration} configuration - The Yarn configuration (used + * for color support detection). + * @param {string} rawRange - The range to format. + * @returns {string} The formatted range. + */ + function formatRangeForDisplay(configuration, rawRange) { + return structUtils.prettyRange(configuration, rawRange); + } + + /** + * Formats a descriptor for a dependency, colorizing it like Yarn would. + * + * @param {YarnConfiguration} configuration - The Yarn configuration (used + * for color support detection). + * @param {YarnDescriptor} descriptor - The source descriptor. + * @returns {string} The formatted descriptor. + */ + function formatSourceForDisplay(configuration, descriptor) { + const locator = structUtils.makeLocator(descriptor, descriptor.range); + return structUtils.prettyLocator(configuration, locator); + } + + /** + * Extracts the patch path from a Yarn descriptor range that represents a + * patched dependency. + * + * @param {string} rawDescriptorRange - The patched descriptor range, e.g. + * `patch:@scope/package@npm%3Aversion#path/to/patch`. + * @returns {string|null} The patch file path, or null if not found. + */ + function extractPatchPath(rawDescriptorRange) { + const hashIndex = rawDescriptorRange.indexOf('#'); + if (hashIndex === -1) { + return null; + } + return rawDescriptorRange.slice(hashIndex + 1); + } + + /** + * Reads the preview builds configuration from the project's package.json, + * adding default values for optional properties. + * + * @param {YarnProject} project - The Yarn project object. + * @returns {PreviewBuildConfigurations} The value of the `previewBuilds` + * key in `package.json`, or an empty object if it isn't present. + */ + function getPreviewBuildConfigurations(project) { + /** @type {{ previewBuilds?: PreviewBuildConfigurations }} */ + const rawManifest = project.topLevelWorkspace.manifest.raw; + const rawPreviewBuilds = rawManifest.previewBuilds ?? {}; + + return Object.entries(rawPreviewBuilds).reduce( + (previewBuilds, [packageName, config]) => { + previewBuilds[packageName] = { + ...config, + previewScope: config.previewScope ?? DEFAULT_PREVIEW_SCOPE, + }; + return previewBuilds; + }, + /** @type {PreviewBuildConfigurations} */ ({}), + ); + } + + /** + * Gets the Yarn descriptor for a dependency from the root manifest. + * + * @param {YarnProject} project - The Yarn project object. + * @param {string} packageName - The package name to look up (e.g., + * "@metamask/foo"). + * @returns {YarnDescriptor | null} The descriptor, or null if not found. + */ + function getDependencyDescriptorFromManifest(project, packageName) { + const ident = structUtils.parseIdent(packageName); + const manifest = project.topLevelWorkspace.manifest; + return manifest.dependencies.get(ident.identHash) ?? null; + } + + /** + * Extracts the version range from a Yarn descriptor range. + * Handles `patch:` protocol, `npm:` protocol, and plain version ranges. + * + * @param {string} rawDescriptorRange - The descriptor range string. + * @returns {string} The extracted version range. + */ + function extractVersionFromRange(rawDescriptorRange) { + // `patch:#::` + if (isPatchedDescriptor(rawDescriptorRange)) { + const parsed = structUtils.parseRange(rawDescriptorRange); + // `source` contains the original descriptor like "@/@npm:" + if (!parsed.source) { + throw new Error( + `Could not extract source from patch range: ${rawDescriptorRange}`, + ); + } + return extractVersionFromRange(parsed.source); + } + + // `npm:` or `npm:@/@` + if (rawDescriptorRange.startsWith('npm:')) { + const { selector } = structUtils.parseRange(rawDescriptorRange); + // Try to parse as a descriptor (for aliased packages like + // `@/@`) + const descriptor = structUtils.tryParseDescriptor(selector, true); + if (descriptor) { + return descriptor.range; + } + // It's just a version range + return selector; + } + + // Plain version range (e.g., `^10.0.0`) + return rawDescriptorRange; + } + + /** + * Ensures that a preview build configuration object has the correct keys + * and that the `type` has a valid value. + * + * @param {PreviewBuildConfiguration} previewBuildConfiguration - The + * preview build configuration to validate. + * @param {string} packageName - The package name for error messages. + * @returns {string | null} Error message if validation fails, otherwise + * `null`. + */ + function validatePreviewBuildConfiguration( + previewBuildConfiguration, + packageName, + ) { + if ( + typeof previewBuildConfiguration !== 'object' || + previewBuildConfiguration === null || + Array.isArray(previewBuildConfiguration) + ) { + return `Invalid preview build configuration for \`${packageName}\`: Expected an object`; + } + + const { type, previewVersion } = previewBuildConfiguration; + const subErrors = []; + + if (!type) { + subErrors.push('Missing \`type\`'); + } + + if (!previewVersion) { + subErrors.push('Missing \`previewVersion\`'); + } + + if (type && type !== 'breaking' && type !== 'non-breaking') { + subErrors.push( + `Invalid \`type\` "${type}" (must be "breaking" or "non-breaking")`, + ); + } + + if (subErrors.length > 0) { + return `Invalid preview build configuration for \`${packageName}\`: ${subErrors.join('; ')}`; + } + + return null; + } + + /** + * Determines what effect a configured preview build has on instances of the + * dependency in the dependency tree. + * + * For breaking changes, only the dependency at the top level will be + * resolved to the preview build. + * + * For non-breaking changes, all instances of the dependency that match the + * major part of the version range specified in `package.json` will be + * resolved to the preview build. + * + * @param {YarnDescriptor} dependency - The Yarn descriptor for a dependency + * in the dependency tree. + * @param {YarnLocator} locator - The Yarn locator for a dependency in the + * dependency tree. + * @param {ValidatedPreviewBuild} validatedPreviewBuild - Information about + * the preview build that corresponds to the dependency. + * @returns {boolean} True if the dependency should be resolved to the + * preview build, false otherwise. + */ + function shouldResolveDependencyToPreviewBuild( + dependency, + locator, + validatedPreviewBuild, + ) { + const { + configuration: { type }, + originalDependencyDescriptor, + } = validatedPreviewBuild; + + if (type === 'breaking') { + return locator.reference === 'workspace:.'; + } + + const rootDependencyMajorVersion = getMajorVersion( + extractVersionFromRange(originalDependencyDescriptor.range), + ); + const dependencyMajorVersion = getMajorVersion( + extractVersionFromRange(dependency.range), + ); + return ( + rootDependencyMajorVersion !== null && + dependencyMajorVersion !== null && + rootDependencyMajorVersion === dependencyMajorVersion + ); + } + + /** + * Constructs a Yarn descriptor for a dependency that will be used to + * resolve it to a preview build. + * + * @param {YarnDescriptor} dependency - The Yarn descriptor for a + * dependency in the dependency tree. + * @param {ValidatedPreviewBuild} validatedPreviewBuild - The validated + * preview build configuration. + * @returns {YarnDescriptor} A new descriptor pointing to the preview build. + */ + function createPreviewBuildDescriptor(dependency, validatedPreviewBuild) { + const { + configuration: { previewScope, previewVersion }, + originalDependencyDescriptor, + } = validatedPreviewBuild; + + const previewPackageName = `@${previewScope}/${dependency.name}`; + const dependencyRange = originalDependencyDescriptor.range; + + let newRange; + if (isPatchedDescriptor(dependencyRange)) { + const patchPath = extractPatchPath(dependencyRange); + if (patchPath) { + newRange = `patch:${previewPackageName}@npm%3A${previewVersion}#${patchPath}`; + } else { + newRange = `npm:${previewPackageName}@${previewVersion}`; + } + } else { + newRange = `npm:${previewPackageName}@${previewVersion}`; + } + + return structUtils.makeDescriptor( + structUtils.makeIdent(dependency.scope, dependency.name), + newRange, + ); + } + + return { + hooks: { + /** + * Validates the `previewBuilds` key in `package.json` before resolution + * starts, ensuring all packages specified there correspond to an entry + * in `dependencies`, and that the configuration objects are valid. + * + * @param {YarnProject} project - The Yarn project. + */ + validateProject: async (project) => { + const previewBuildConfigurations = + getPreviewBuildConfigurations(project); + const packageNames = Object.keys(previewBuildConfigurations); + + if (packageNames.length === 0) { + return; + } + + for (const packageName of packageNames) { + const previewBuildConfiguration = + previewBuildConfigurations[packageName]; + + const error = validatePreviewBuildConfiguration( + previewBuildConfiguration, + packageName, + ); + if (error) { + errors.add(error); + continue; + } + + const originalDependencyDescriptor = + getDependencyDescriptorFromManifest(project, packageName); + if (!originalDependencyDescriptor) { + errors.add( + `\`${packageName}\` is configured in \`previewBuilds\`, but not found in \`dependencies\``, + ); + continue; + } + + validatedPreviewBuilds.set(packageName, { + configuration: previewBuildConfiguration, + originalDependencyDescriptor, + }); + } + }, + + /** + * Yarn calls this during the resolution phase for each dependency in + * the dependency tree. + * + * This hook allows us to resolve `@metamask` packages to associated + * preview builds. + * + * @param {YarnDescriptor} dependency - The dependency being resolved. + * @param {YarnProject} project - The Yarn project. + * @param {YarnLocator} locator - The parent package locator. + * @returns {Promise} The resolved descriptor. + */ + reduceDependency: async (dependency, project, locator) => { + if (dependency.scope !== METAMASK_NPM_SCOPE) { + return dependency; + } + + const packageName = `@${dependency.scope}/${dependency.name}`; + + const validatedPreviewBuild = validatedPreviewBuilds.get(packageName); + if (!validatedPreviewBuild) { + return dependency; + } + + if ( + !shouldResolveDependencyToPreviewBuild( + dependency, + locator, + validatedPreviewBuild, + ) + ) { + return dependency; + } + + const previewBuildDescriptor = createPreviewBuildDescriptor( + dependency, + validatedPreviewBuild, + ); + + const resolutionKey = dependency.descriptorHash; + resolutions.set(resolutionKey, { + sourceDescriptor: dependency, + targetDescriptor: previewBuildDescriptor, + type: validatedPreviewBuild.configuration.type, + }); + + return previewBuildDescriptor; + }, + + /** + * Yarn calls this hook after all packages have been installed. + * + * This hook is used to print summary information and clear caches. + * + * @param {YarnProject} project - The Yarn project. + */ + afterAllInstalled: (project) => { + const { configuration } = project; + + if (errors.size > 0) { + console.log(''); + console.error( + `[plugin-preview-builds] Preview build configurations were not processed due to the following errors:`, + ); + errors.forEach((error) => { + console.error(` - ${error}`); + }); + console.log(''); + } + + if (resolutions.size > 0) { + const workspaceLocator = project.topLevelWorkspace.anchoredLocator; + console.log(''); + console.log( + `[plugin-preview-builds] The following dependencies were mapped to preview builds:`, + ); + for (const { + sourceDescriptor, + targetDescriptor, + type, + } of resolutions.values()) { + const source = formatSourceForDisplay( + configuration, + sourceDescriptor, + ); + const target = formatRangeForDisplay( + configuration, + targetDescriptor.range, + ); + const isPatched = isPatchedDescriptor(targetDescriptor.range); + const arrow = isPatched ? '\n -> ' : ' -> '; + if (type === 'breaking') { + const prettyWorkspace = structUtils.prettyLocator( + configuration, + workspaceLocator, + ); + console.log(`- ${prettyWorkspace}/${source}${arrow}${target}`); + } else { + console.log(`- ${source}${arrow}${target}`); + } + } + console.log(''); + } + + validatedPreviewBuilds.clear(); + resolutions.clear(); + errors.clear(); + }, + }, + }; + }, +}; diff --git a/.yarnrc.yml b/.yarnrc.yml index a61c526d219..4328c89b949 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -6,6 +6,10 @@ enableScripts: false nodeLinker: node-modules +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-preview-builds.cjs + spec: "@yarnpkg/plugin-preview-builds" + npmAuditIgnoreAdvisories: - 1109627 # TODO: Upgrade @react-native-community/cli to 17.0.1+ when ready. Suppressing for now to unblock CI. - 1112455 # lodash prototype pollution in _.unset and _.omit. No fix available yet (latest is 4.17.21, affected <=4.17.22). Suppressing for now to unblock CI. https://github.com/advisories/GHSA-xxjr-mmjv-4gpg diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d51a248853..679adc91354 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [7.62.2] + +### Fixed + +- fix: cherry-pick 429 rate limiting fix with coin naming convention (#25443) +- fix(perps): potential rate limit on close positions (#25456) + ## [7.62.1] ### Fixed @@ -9910,7 +9917,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#957](https://github.com/MetaMask/metamask-mobile/pull/957): fix timeouts (#957) - [#954](https://github.com/MetaMask/metamask-mobile/pull/954): Bugfix: onboarding navigation (#954) -[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.62.1...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.62.2...HEAD +[7.62.2]: https://github.com/MetaMask/metamask-mobile/compare/v7.62.1...v7.62.2 [7.62.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.62.0...v7.62.1 [7.62.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.61.6...v7.62.0 [7.61.6]: https://github.com/MetaMask/metamask-mobile/compare/v7.61.5...v7.61.6 diff --git a/app/components/UI/Card/components/Onboarding/RegionSelectorModal.test.tsx b/app/components/UI/Card/components/Onboarding/RegionSelectorModal.test.tsx index 6b1954c58c2..bf68e1de9b6 100644 --- a/app/components/UI/Card/components/Onboarding/RegionSelectorModal.test.tsx +++ b/app/components/UI/Card/components/Onboarding/RegionSelectorModal.test.tsx @@ -91,91 +91,7 @@ jest.mock( }, ); -// Mock BottomSheetHeader -jest.mock( - '../../../../../component-library/components/BottomSheets/BottomSheetHeader', - () => { - const React = jest.requireActual('react'); - const { View, TouchableOpacity } = jest.requireActual('react-native'); - - return ({ - children, - onClose, - }: { - children: React.ReactNode; - onClose?: () => void; - }) => - React.createElement( - View, - { testID: 'bottom-sheet-header' }, - children, - onClose && - React.createElement( - TouchableOpacity, - { testID: 'bottom-sheet-close-button', onPress: onClose }, - 'Close', - ), - ); - }, -); - -// Mock ListItemSelect -jest.mock( - '../../../../../component-library/components/List/ListItemSelect', - () => { - const React = jest.requireActual('react'); - const { TouchableOpacity } = jest.requireActual('react-native'); - - return ({ - children, - onPress, - isSelected, - testID, - }: { - children: React.ReactNode; - onPress: () => void; - isSelected?: boolean; - accessibilityRole?: string; - accessible?: boolean; - testID?: string; - }) => - React.createElement( - TouchableOpacity, - { - testID: testID || 'list-item-select', - onPress, - accessibilityState: { selected: isSelected }, - }, - children, - ); - }, -); - -// Mock ListItemColumn -jest.mock( - '../../../../../component-library/components/List/ListItemColumn', - () => { - const React = jest.requireActual('react'); - const { View } = jest.requireActual('react-native'); - - const MockListItemColumn = ({ - children, - }: { - children: React.ReactNode; - widthType?: string; - }) => React.createElement(View, { testID: 'list-item-column' }, children); - - return { - __esModule: true, - default: MockListItemColumn, - WidthType: { - Fill: 'Fill', - }, - }; - }, -); - -// Mock TextFieldSearch +// Mock TextFieldSearch - needed because component has internal elements (clear button) that need testIDs jest.mock( '../../../../../component-library/components/Form/TextFieldSearch', () => { @@ -220,61 +136,6 @@ jest.mock( }, ); -// Mock design system components -jest.mock('@metamask/design-system-react-native', () => { - const React = jest.requireActual('react'); - const { View, Text } = jest.requireActual('react-native'); - - return { - Box: ({ - children, - testID, - twClassName, - ...props - }: { - children?: React.ReactNode; - testID?: string; - twClassName?: string; - flexDirection?: string; - alignItems?: string; - [key: string]: unknown; - }) => - React.createElement( - View, - { testID: testID || 'box', 'data-tw-class': twClassName, ...props }, - children, - ), - Text: ({ - children, - testID, - variant, - ...props - }: { - children?: React.ReactNode; - testID?: string; - variant?: string; - [key: string]: unknown; - }) => - React.createElement( - Text, - { testID: testID || 'text', 'data-variant': variant, ...props }, - children, - ), - TextVariant: { - HeadingMd: 'HeadingMd', - BodyLg: 'BodyLg', - BodyMd: 'BodyMd', - }, - BoxFlexDirection: { - Row: 'row', - Column: 'column', - }, - BoxAlignItems: { - Center: 'center', - }, - }; -}); - // Mock FlatList from react-native-gesture-handler jest.mock('react-native-gesture-handler', () => { const RN = jest.requireActual('react-native'); @@ -574,7 +435,7 @@ describe('RegionSelectorModal', () => { , ); - const closeButton = getByTestId('bottom-sheet-close-button'); + const closeButton = getByTestId('region-selector-close-button'); await act(async () => { fireEvent.press(closeButton); diff --git a/app/components/UI/Card/components/Onboarding/RegionSelectorModal.tsx b/app/components/UI/Card/components/Onboarding/RegionSelectorModal.tsx index 8b5135310fc..cde22378546 100644 --- a/app/components/UI/Card/components/Onboarding/RegionSelectorModal.tsx +++ b/app/components/UI/Card/components/Onboarding/RegionSelectorModal.tsx @@ -11,7 +11,7 @@ import Fuse from 'fuse.js'; import BottomSheet, { BottomSheetRef, } from '../../../../../component-library/components/BottomSheets/BottomSheet'; -import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import HeaderCenter from '../../../../../component-library/components-temp/HeaderCenter'; import ListItemSelect from '../../../../../component-library/components/List/ListItemSelect'; import ListItemColumn, { WidthType, @@ -214,11 +214,11 @@ function RegionSelectorModal() { keyboardAvoidingViewEnabled={false} testID="region-selector-modal" > - - - {strings('card.card_onboarding.region_selector.title')} - - + { shouldNavigateBack > - sheetRef.current?.onCloseBottomSheet()} - > - {strings('wallet.networks')} - + /> { const { getByText } = render(); // Assert - expect(getByText('Transaction not found')).toBeTruthy(); + expect(getByText('Transaction not found')).toBeOnTheScreen(); }); it('should format date correctly', () => { diff --git a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsFundingTransactionView.tsx b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsFundingTransactionView.tsx index e97f84f29ff..0f9752229ff 100644 --- a/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsFundingTransactionView.tsx +++ b/app/components/UI/Perps/Views/PerpsTransactionsView/PerpsFundingTransactionView.tsx @@ -4,7 +4,7 @@ import { useRoute, } from '@react-navigation/native'; import { BigNumber } from 'bignumber.js'; -import React from 'react'; +import React, { useLayoutEffect } from 'react'; import { ScrollView, View } from 'react-native'; import { useSelector } from 'react-redux'; import { PerpsTransactionSelectorsIDs } from '../../Perps.testIds'; @@ -21,7 +21,7 @@ import Text, { import { useStyles } from '../../../../../component-library/hooks'; import { selectSelectedInternalAccountByScope } from '../../../../../selectors/multichainAccounts/accounts'; import ScreenView from '../../../../Base/ScreenView'; -import { getPerpsTransactionsDetailsNavbar } from '../../../Navbar'; +import HeaderCenter from '../../../../../component-library/components-temp/HeaderCenter'; import PerpsTransactionDetailAssetHero from '../../components/PerpsTransactionDetailAssetHero'; import { usePerpsBlockExplorerUrl } from '../../hooks'; import { PerpsNavigationParamList } from '../../types/navigation'; @@ -49,10 +49,17 @@ const PerpsFundingTransactionView: React.FC = () => { // Get transaction from route params const transaction = route.params?.transaction as PerpsTransaction; + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: false, + }); + }, [navigation]); + // Check if transaction exists before proceeding if (!transaction) { return ( + navigation.goBack()} /> {strings('perps.transactions.not_found')} @@ -60,11 +67,6 @@ const PerpsFundingTransactionView: React.FC = () => { ); } - // Set navigation title - navigation.setOptions( - getPerpsTransactionsDetailsNavbar(navigation, transaction.title), - ); - const handleViewOnBlockExplorer = () => { if (!selectedInternalAccount) { return; @@ -109,6 +111,11 @@ const PerpsFundingTransactionView: React.FC = () => { return ( + navigation.goBack()} + /> { amount: transaction?.order?.size ?? '0', }); + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: false, + }); + }, [navigation]); + if (!transaction) { return ( + navigation.goBack()} /> {strings('perps.transactions.not_found')} @@ -60,11 +67,6 @@ const PerpsOrderTransactionView: React.FC = () => { ); } - // Set navigation title - navigation.setOptions( - getPerpsTransactionsDetailsNavbar(navigation, transaction.title), - ); - const handleViewOnBlockExplorer = () => { if (!selectedInternalAccount) { return; @@ -123,6 +125,11 @@ const PerpsOrderTransactionView: React.FC = () => { return ( + navigation.goBack()} + /> { [transaction?.asset], ); - navigation.setOptions( - getPerpsTransactionsDetailsNavbar( - navigation, - transaction?.fill?.shortTitle || '', - ), - ); + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: false, + }); + }, [navigation]); if (!transaction) { // Handle missing transaction data return ( + navigation.goBack()} /> {strings('perps.transactions.not_found')} @@ -174,6 +174,11 @@ const PerpsPositionTransactionView: React.FC = () => { return ( + navigation.goBack()} + includesTopInset + /> = () => { const route = useRoute>(); const { activity } = route.params || {}; - const { colors } = useTheme(); const tw = useTailwind(); - const insets = useSafeAreaInsets(); // Determine activity type for analytics const activityType = useMemo(() => { @@ -268,39 +258,6 @@ const PredictActivityDetails: React.FC = () => { }; }, [activity]); - const renderHeader = () => ( - - - - - - {activityDetails?.headerTitle ?? - strings('predict.transactions.activity_details')} - - - - ); - const renderDetailRow = ( label: string, value: string, @@ -417,8 +374,18 @@ const PredictActivityDetails: React.FC = () => { testID={PredictActivityDetailsSelectorsIDs.CONTAINER} > - - {renderHeader()} + + {renderAmountDisplay()} {renderPredictionMarketDetails()} {renderTransactionDetails()} diff --git a/app/components/UI/Ramp/Aggregator/Views/OrderDetails/OrderDetails.test.tsx b/app/components/UI/Ramp/Aggregator/Views/OrderDetails/OrderDetails.test.tsx index cd83ab04073..7f26ff5c020 100644 --- a/app/components/UI/Ramp/Aggregator/Views/OrderDetails/OrderDetails.test.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/OrderDetails/OrderDetails.test.tsx @@ -187,6 +187,17 @@ describe('OrderDetails', () => { expect(screen.toJSON()).toMatchSnapshot(); }); + it('renders header with back navigation capability on empty order state', async () => { + mockUseParamsValues = { + ...mockUseParamsDefaultValues, + orderId: 'invalid-id', + }; + render(OrderDetails); + + expect(screen.getByTestId('header')).toBeOnTheScreen(); + expect(screen.getByText('Order details')).toBeOnTheScreen(); + }); + it('redirects to send transaction page when user is redirected back from a provider for a sell order', async () => { const testOrder = { ...mockOrder, @@ -456,6 +467,21 @@ describe('OrderDetails', () => { ); }); + it('renders header with back navigation capability on error state', async () => { + const createdOrder = { + ...mockOrder, + orderType: OrderOrderTypeEnum.Sell, + state: FIAT_ORDER_STATES.CREATED, + }; + (processFiatOrder as jest.Mock).mockImplementationOnce(() => { + throw new Error('An error occurred'); + }); + await waitFor(() => render(OrderDetails, [createdOrder])); + + expect(screen.getByTestId('header')).toBeOnTheScreen(); + expect(screen.getByText('Order details')).toBeOnTheScreen(); + }); + it('renders the support links if the provider has them', async () => { const testOrder = { ...mockOrder, diff --git a/app/components/UI/Ramp/Aggregator/Views/OrderDetails/OrderDetails.tsx b/app/components/UI/Ramp/Aggregator/Views/OrderDetails/OrderDetails.tsx index 8a330394b6d..9d7ac94aaef 100644 --- a/app/components/UI/Ramp/Aggregator/Views/OrderDetails/OrderDetails.tsx +++ b/app/components/UI/Ramp/Aggregator/Views/OrderDetails/OrderDetails.tsx @@ -1,4 +1,9 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { + useCallback, + useEffect, + useLayoutEffect, + useState, +} from 'react'; import { ActivityIndicator, RefreshControl } from 'react-native'; import { useDispatch, useSelector } from 'react-redux'; import { useNavigation } from '@react-navigation/native'; @@ -21,7 +26,7 @@ import { updateFiatOrder, } from '../../../../../../reducers/fiatOrders'; import { strings } from '../../../../../../../locales/i18n'; -import { getDepositNavbarOptions } from '../../../../Navbar'; +import HeaderCenter from '../../../../../../component-library/components-temp/HeaderCenter'; import Routes from '../../../../../../constants/navigation/Routes'; import { processFiatOrder } from '../../../index'; import { @@ -67,17 +72,11 @@ const OrderDetails = () => { const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshingInterval, setIsRefreshingInterval] = useState(false); - useEffect(() => { - navigation.setOptions( - getDepositNavbarOptions( - navigation, - { - title: strings('fiat_on_ramp_aggregator.order_details.details_main'), - }, - theme, - ), - ); - }, [theme, navigation]); + useLayoutEffect(() => { + navigation.setOptions({ + headerShown: false, + }); + }, [navigation]); const navigateToSendTransaction = useCallback(() => { if (order?.id) { @@ -213,12 +212,25 @@ const OrderDetails = () => { ); if (!order) { - return ; + return ( + + navigation.goBack()} + /> + + ); } if (isLoading) { return ( + navigation.goBack()} + /> @@ -231,6 +243,11 @@ const OrderDetails = () => { if (error) { return ( + navigation.goBack()} + /> { return ( + navigation.goBack()} + /> - - - - - - - - - - - - - - - - Order details - - - - - - - - - + + + + + + + + + + + + + + Order details + + + + + + + + `; @@ -1451,247 +1448,74 @@ exports[`OrderDetails renders a completed order 1`] = ` ] } > - - + - - - - - - - - - - - - - Order details - - - - - - - - - - - - - + + + + + + + + + + + + + + Order details + + + + + + + + `; @@ -2901,222 +2895,49 @@ exports[`OrderDetails renders a created order 1`] = ` ] } > - - - - - - - - - - - - - - - Order details - - - - - - - - - - - - + + + + + + + + + + + + + + Order details + + + + + + + + `; @@ -4329,240 +4320,67 @@ exports[`OrderDetails renders a failed order 1`] = ` ] } > - - + - - - - - - - - - - - - - Order details - - - - - - - - - - - - - + + + + + + + + + + + + + + Order details + + + + + + + + `; @@ -5761,215 +5749,42 @@ exports[`OrderDetails renders a pending order 1`] = ` ] } > - - - - - - - - - - - - - - - - Order details - - - - - - - - - - - + + + + + + + + + + + + + + Order details + + + + + + + + `; @@ -7189,222 +7174,49 @@ exports[`OrderDetails renders an empty screen layout if there is no order 1`] = ] } > - - - - - - - - - - - - - - - Order details - - - - - - - - - - - - @@ -7568,7 +7381,163 @@ exports[`OrderDetails renders an empty screen layout if there is no order 1`] = undefined, ] } - /> + > + + + + + + + + + + + + + + Order details + + + + + + + + @@ -7578,6 +7547,20 @@ exports[`OrderDetails renders an empty screen layout if there is no order 1`] = + `; @@ -7601,179 +7584,6 @@ exports[`OrderDetails renders an error screen if a CREATED order cannot be polle ] } > - - - - - - - - - - - - - - - - Order details - - - - - - - - - + + + + + + + + + + + + + + Order details + + + + + + + + `; @@ -8202,179 +8182,6 @@ exports[`OrderDetails renders non-transacted orders 1`] = ` ] } > - - - - - - - - - - - - - - - - Order details - - - - - - - - - + + + + + + + + + + + + + + Order details + + + + + + + - - - - - - - - - - - -`; - -exports[`OrderDetails renders the support links if the provider has them 1`] = ` - - - - - - - - - - + + - - - - Order details - - - - - - - - + + + + + +`; + +exports[`OrderDetails renders the support links if the provider has them 1`] = ` + + + + + + + + + + + + + + + + Order details + + + + + + + + `; @@ -11191,179 +11165,6 @@ exports[`OrderDetails renders transacted orders that do not have timeDescription ] } > - - - - - - - - - - - - - - - - Order details - - - - - - - - - + + + + + + + + + + + + + + Order details + + + + + + + + `; @@ -12619,179 +12590,6 @@ exports[`OrderDetails renders transacted orders that have timeDescriptionPending ] } > - - - - - - - - - - - - - - - - Order details - - - - - - - - - + + + + + + + + + + + + + + Order details + + + + + + + + `; diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.test.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.test.tsx index fcbb18ed2c0..12a8407e9ba 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.test.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.test.tsx @@ -187,5 +187,14 @@ describe('StateSelectorModal Component', () => { expect(getByText('No states match "Nonexistent"')).toBeOnTheScreen(); }); + + it('closes the modal when close button is pressed', () => { + const { getByTestId } = renderWithProvider(StateSelectorModal); + + const closeButton = getByTestId('state-selector-close-button'); + fireEvent.press(closeButton); + + expect(mockGoBack).toHaveBeenCalled(); + }); }); }); diff --git a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx index 395f8096d48..57c823ffd3f 100644 --- a/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx +++ b/app/components/UI/Ramp/Deposit/Views/Modals/StateSelectorModal/StateSelectorModal.tsx @@ -11,7 +11,7 @@ import Text, { import BottomSheet, { BottomSheetRef, } from '../../../../../../../component-library/components/BottomSheets/BottomSheet'; -import BottomSheetHeader from '../../../../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import HeaderCenter from '../../../../../../../component-library/components-temp/HeaderCenter'; import ListItemSelect from '../../../../../../../component-library/components/List/ListItemSelect'; import ListItemColumn, { WidthType, @@ -156,11 +156,11 @@ function StateSelectorModal() { return ( - sheetRef.current?.onCloseBottomSheet()}> - - {strings('deposit.state_modal.select_a_state')} - - + sheetRef.current?.onCloseBottomSheet()} + closeButtonProps={{ testID: 'state-selector-close-button' }} + /> - - Select a state - + + Select a state + + - - - + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "backgroundColor": "transparent", + "borderRadius": 2, + "height": 32, + "justifyContent": "center", + "opacity": 1, + "width": 32, + }, + undefined, + ] + } + testID="state-selector-close-button" + > + + + @@ -1237,11 +1300,11 @@ exports[`StateSelectorModal Component Snapshot Tests renders empty state when no "flexDirection": "row", "gap": 16, "height": 56, + "paddingLeft": 8, + "paddingRight": 8, }, false, - { - "paddingHorizontal": 16, - }, + undefined, ] } testID="header" @@ -1265,57 +1328,120 @@ exports[`StateSelectorModal Component Snapshot Tests renders empty state when no ] } > - - Select a state - + + Select a state + + - - - + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "backgroundColor": "transparent", + "borderRadius": 2, + "height": 32, + "justifyContent": "center", + "opacity": 1, + "width": 32, + }, + undefined, + ] + } + testID="state-selector-close-button" + > + + + @@ -1981,11 +2107,11 @@ exports[`StateSelectorModal Component Snapshot Tests renders filtered state when "flexDirection": "row", "gap": 16, "height": 56, + "paddingLeft": 8, + "paddingRight": 8, }, false, - { - "paddingHorizontal": 16, - }, + undefined, ] } testID="header" @@ -2009,57 +2135,120 @@ exports[`StateSelectorModal Component Snapshot Tests renders filtered state when ] } > - - Select a state - + + Select a state + + - - - + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "backgroundColor": "transparent", + "borderRadius": 2, + "height": 32, + "justifyContent": "center", + "opacity": 1, + "width": 32, + }, + undefined, + ] + } + testID="state-selector-close-button" + > + + + @@ -2781,11 +2970,11 @@ exports[`StateSelectorModal Component Snapshot Tests renders filtered state when "flexDirection": "row", "gap": 16, "height": 56, + "paddingLeft": 8, + "paddingRight": 8, }, false, - { - "paddingHorizontal": 16, - }, + undefined, ] } testID="header" @@ -2809,57 +2998,120 @@ exports[`StateSelectorModal Component Snapshot Tests renders filtered state when ] } > - - Select a state - + + Select a state + + - - - + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "backgroundColor": "transparent", + "borderRadius": 2, + "height": 32, + "justifyContent": "center", + "opacity": 1, + "width": 32, + }, + undefined, + ] + } + testID="state-selector-close-button" + > + + + @@ -3581,11 +3833,11 @@ exports[`StateSelectorModal Component Snapshot Tests renders initial state corre "flexDirection": "row", "gap": 16, "height": 56, + "paddingLeft": 8, + "paddingRight": 8, }, false, - { - "paddingHorizontal": 16, - }, + undefined, ] } testID="header" @@ -3609,57 +3861,120 @@ exports[`StateSelectorModal Component Snapshot Tests renders initial state corre ] } > - - Select a state - + + Select a state + + - - - + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "backgroundColor": "transparent", + "borderRadius": 2, + "height": 32, + "justifyContent": "center", + "opacity": 1, + "width": 32, + }, + undefined, + ] + } + testID="state-selector-close-button" + > + + + @@ -4644,11 +4959,11 @@ exports[`StateSelectorModal Component Snapshot Tests renders partial search resu "flexDirection": "row", "gap": 16, "height": 56, + "paddingLeft": 8, + "paddingRight": 8, }, false, - { - "paddingHorizontal": 16, - }, + undefined, ] } testID="header" @@ -4672,57 +4987,120 @@ exports[`StateSelectorModal Component Snapshot Tests renders partial search resu ] } > - - Select a state - + + Select a state + + - - - + accessibilityValue={ + { + "max": undefined, + "min": undefined, + "now": undefined, + "text": undefined, + } + } + accessible={true} + collapsable={false} + focusable={true} + onBlur={[Function]} + onClick={[Function]} + onFocus={[Function]} + onResponderGrant={[Function]} + onResponderMove={[Function]} + onResponderRelease={[Function]} + onResponderTerminate={[Function]} + onResponderTerminationRequest={[Function]} + onStartShouldSetResponder={[Function]} + style={ + [ + { + "alignItems": "center", + "backgroundColor": "transparent", + "borderRadius": 2, + "height": 32, + "justifyContent": "center", + "opacity": 1, + "width": 32, + }, + undefined, + ] + } + testID="state-selector-close-button" + > + + + diff --git a/app/components/UI/TransactionElement/index.js b/app/components/UI/TransactionElement/index.js index 0f7665bcaf6..738137ff4cb 100644 --- a/app/components/UI/TransactionElement/index.js +++ b/app/components/UI/TransactionElement/index.js @@ -21,6 +21,7 @@ import { TRANSACTION_TYPES } from '../../../util/transactions'; import ListItem from '../../Base/ListItem'; import StatusText from '../../Base/StatusText'; import DetailsModal from '../../Base/DetailsModal'; +import HeaderCenter from '../../../component-library/components-temp/HeaderCenter'; import { isTestNet } from '../../../util/networks'; import { weiHexToGweiDec } from '@metamask/controller-utils'; import { @@ -770,12 +771,12 @@ class TransactionElement extends PureComponent { backdropOpacity={1} > - - - {strings('transactions.import_wallet_label')} - - - + {strings('transactions.import_wallet_tip')} diff --git a/bitrise.yml b/bitrise.yml index 84b0a0ea430..57bfbd3c1e1 100644 --- a/bitrise.yml +++ b/bitrise.yml @@ -1822,6 +1822,27 @@ workflows: - content: |- #!/usr/bin/env bash envman add --key SKIP_CCACHE_UPLOAD --value "true" + - script@1: + title: "[Phase 1.5] Verify builds.yml config" + is_skippable: true + inputs: + - content: |- + #!/usr/bin/env bash + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Phase 1.5: Parallel Validation ║" + echo "║ Comparing Bitrise env vars with builds.yml config ║" + echo "╚════════════════════════════════════════════════════════════╝" + + # Set defaults if not already set + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'e2e'} + + # Run verification (auto-detects build from env vars) + # Using --verbose to see all checks in build logs + node scripts/verify-build-config.js --verbose || true + + # Note: Currently running without --strict + # Once verified, change to: node scripts/verify-build-config.js --strict - script@1: title: Run detox build timeout: 1800 @@ -2252,6 +2273,27 @@ workflows: #!/usr/bin/env bash sudo apt update sudo apt install libicu-dev -y + - script@1: + title: "[Phase 1.5] Verify builds.yml config" + is_skippable: true + inputs: + - content: |- + #!/usr/bin/env bash + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Phase 1.5: Parallel Validation ║" + echo "║ Comparing Bitrise env vars with builds.yml config ║" + echo "╚════════════════════════════════════════════════════════════╝" + + # Set defaults if not already set + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'production'} + + # Run verification (auto-detects build from env vars) + # Using --verbose to see all checks in build logs + node scripts/verify-build-config.js --verbose || true + + # Note: Currently running without --strict + # Once verified, change to: node scripts/verify-build-config.js --strict - script@1: title: Build Android Binary is_always_run: false @@ -2448,6 +2490,27 @@ workflows: - content: |- #!/usr/bin/env bash envman add --key SKIP_CCACHE_UPLOAD --value "true" + - script@1: + title: "[Phase 1.5] Verify builds.yml config" + is_skippable: true + inputs: + - content: |- + #!/usr/bin/env bash + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Phase 1.5: Parallel Validation ║" + echo "║ Comparing Bitrise env vars with builds.yml config ║" + echo "╚════════════════════════════════════════════════════════════╝" + + # Set defaults if not already set + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'e2e'} + + # Run verification (auto-detects build from env vars) + # Using --verbose to see all checks in build logs + node scripts/verify-build-config.js --verbose || true + + # Note: Currently running without --strict + # Once verified, change to: node scripts/verify-build-config.js --strict - script@1: title: Run detox build timeout: 1800 @@ -2911,6 +2974,27 @@ workflows: - certificate-and-profile-installer@1: { run_if: '{{not (enveq "IS_SIM_BUILD" "true")}}' # Only run for physical builds } + - script@1: + title: "[Phase 1.5] Verify builds.yml config" + is_skippable: true + inputs: + - content: |- + #!/usr/bin/env bash + echo "╔════════════════════════════════════════════════════════════╗" + echo "║ Phase 1.5: Parallel Validation ║" + echo "║ Comparing Bitrise env vars with builds.yml config ║" + echo "╚════════════════════════════════════════════════════════════╝" + + # Set defaults if not already set + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'production'} + + # Run verification (auto-detects build from env vars) + # Using --verbose to see all checks in build logs + node scripts/verify-build-config.js --verbose || true + + # Note: Currently running without --strict + # Once verified, change to: node scripts/verify-build-config.js --strict - script@1: title: iOS Sourcemaps & Build is_always_run: false @@ -3115,10 +3199,6 @@ workflows: - SENTRY_DISABLE_AUTO_UPLOAD: 'false' after_run: - build_ios_main_rc - # TODO: Remove this workflow once new build configuration is consolidated - build_ios_flask_release: - after_run: - - build_ios_flask_prod build_ios_flask_prod: envs: - CONFIGURATION: 'Release' diff --git a/e2e/specs/identity/account-syncing/multi-srp.spec.ts b/e2e/specs/identity/account-syncing/multi-srp.spec.ts index 8db323e4a7e..d2a0d2c75e4 100644 --- a/e2e/specs/identity/account-syncing/multi-srp.spec.ts +++ b/e2e/specs/identity/account-syncing/multi-srp.spec.ts @@ -10,7 +10,10 @@ import { UserStorageMockttpControllerEvents, UserStorageMockttpController, } from '../utils/user-storage/userStorageMockttpController'; -import { goToImportSrp, inputSrp } from '../../multisrp/utils'; +import { + goToImportSrp, + inputSrp, +} from '../../../../tests/flows/accounts.flow.ts'; import ImportSrpView from '../../../pages/importSrp/ImportSrpView'; import { IDENTITY_TEAM_SEED_PHRASE_2 } from '../utils/constants'; import { createUserStorageController } from '../utils/mocks'; diff --git a/e2e/specs/multichain-accounts/export-credentials.spec.ts b/e2e/specs/multichain-accounts/export-credentials.spec.ts index 399c1c06cc4..41d33768cba 100644 --- a/e2e/specs/multichain-accounts/export-credentials.spec.ts +++ b/e2e/specs/multichain-accounts/export-credentials.spec.ts @@ -5,7 +5,7 @@ import { withMultichainAccountDetailsEnabledFixtures, } from './common'; import AccountDetails from '../../pages/MultichainAccounts/AccountDetails'; -import { completeSrpQuiz } from '../multisrp/utils'; +import { completeSrpQuiz } from '../../../tests/flows/accounts.flow.ts'; import { defaultOptions } from '../../../tests/seeder/anvil-manager'; import TestHelpers from '../../helpers'; diff --git a/e2e/specs/quarantine/create-wallet-account.failing.ts b/e2e/specs/quarantine/create-wallet-account.failing.ts index c9629c2b373..99c12efd1b1 100644 --- a/e2e/specs/quarantine/create-wallet-account.failing.ts +++ b/e2e/specs/quarantine/create-wallet-account.failing.ts @@ -6,7 +6,7 @@ import { withMultichainAccountDetailsV2EnabledFixtures } from '../multichain-acc import AccountDetails from '../../pages/MultichainAccounts/AccountDetails.js'; import AddressList from '../../pages/MultichainAccounts/AddressList.js'; import { defaultGanacheOptions } from '../../../tests/framework/Constants.js'; -import { completeSrpQuiz } from '../multisrp/utils.js'; +import { completeSrpQuiz } from '../../../tests/flows/accounts.flow.js'; // Quarantining, See open ticket here: https://github.com/MetaMask/metamask-mobile/issues/21429 describe(SmokeAccounts('Create wallet accounts'), () => { diff --git a/package.json b/package.json index 4cf9f0dfe42..3b7e93ac490 100644 --- a/package.json +++ b/package.json @@ -606,6 +606,7 @@ "jest": "^29.7.0", "jest-junit": "^15.0.0", "jetifier": "2.0.0", + "js-yaml": "^4.1.0", "koa": "^2.14.2", "lint-staged": "10.5.4", "listr2": "^8.0.2", diff --git a/scripts/apply-build-config.js b/scripts/apply-build-config.js new file mode 100755 index 00000000000..cfe99795b6e --- /dev/null +++ b/scripts/apply-build-config.js @@ -0,0 +1,108 @@ +#!/usr/bin/env node +/** + * Loads build configuration from builds.yml and sets environment variables. + * Simple, no magic inheritance - each build has its full config via YAML anchors. + * + * Usage: + * node scripts/apply-build-config.js main-prod + * node scripts/apply-build-config.js main-dev --export # outputs for shell eval + */ + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const BUILDS_PATH = path.join(__dirname, '../.github/builds.yml'); + +function loadConfig(buildName) { + if (!fs.existsSync(BUILDS_PATH)) { + throw new Error('.github/builds.yml not found'); + } + + const config = yaml.load(fs.readFileSync(BUILDS_PATH, 'utf8')); + + if (!config.builds || !config.builds[buildName]) { + const available = Object.keys(config.builds || {}).join(', '); + throw new Error(`Build "${buildName}" not found. Available: ${available}`); + } + + return config.builds[buildName]; +} + +function applyConfig(buildName) { + const config = loadConfig(buildName); + + // Set all env vars from config.env + if (config.env) { + Object.entries(config.env).forEach(([key, value]) => { + process.env[key] = String(value); + }); + } + + // Set code fencing features + if (config.code_fencing) { + process.env.CODE_FENCING_FEATURES = JSON.stringify(config.code_fencing); + } + + // Set remote feature flag defaults (seeded into RemoteFeatureFlagController) + if (config.remote_feature_flags) { + process.env.REMOTE_FEATURE_FLAG_DEFAULTS = JSON.stringify( + config.remote_feature_flags, + ); + } + + return config; +} + +// Export for shell (used in CI) +function exportForShell(buildName) { + const config = loadConfig(buildName); + const lines = []; + + if (config.env) { + Object.entries(config.env).forEach(([key, value]) => { + lines.push(`export ${key}="${String(value)}"`); + }); + } + + if (config.code_fencing) { + lines.push( + `export CODE_FENCING_FEATURES='${JSON.stringify(config.code_fencing)}'`, + ); + } + + if (config.remote_feature_flags) { + lines.push( + `export REMOTE_FEATURE_FLAG_DEFAULTS='${JSON.stringify(config.remote_feature_flags)}'`, + ); + } + + return lines.join('\n'); +} + +// CLI +if (require.main === module) { + const args = process.argv.slice(2); + const buildName = args.find((a) => !a.startsWith('--')); + const exportMode = args.includes('--export'); + + if (!buildName) { + console.error('Usage: node apply-build-config.js [--export]'); + console.error('Example: node apply-build-config.js main-prod'); + process.exit(1); + } + + try { + if (exportMode) { + console.log(exportForShell(buildName)); + } else { + applyConfig(buildName); + console.log(`✅ Applied config for ${buildName}`); + } + } catch (error) { + console.error(`❌ ${error.message}`); + process.exit(1); + } +} + +module.exports = { loadConfig, applyConfig, exportForShell }; diff --git a/scripts/build.sh b/scripts/build.sh index 8d1f91ccba4..b4582b5c2b6 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -178,93 +178,6 @@ remapEnvVariable() { echo "Successfully remapped $old_var_name to $new_var_name." } -# Create .env file from environment variables and optionally export to GITHUB_ENV -createEnvFile() { - echo "📝 Creating .env file from environment variables..." - - # List of environment variable names to export - ENV_VARS=( - "MM_MUSD_CONVERSION_FLOW_ENABLED" - "MM_NETWORK_UI_REDESIGN_ENABLED" - "MM_NOTIFICATIONS_UI_ENABLED" - "MM_PERMISSIONS_SETTINGS_V1_ENABLED" - "MM_PERPS_BLOCKED_REGIONS" - "MM_PERPS_ENABLED" - "MM_PERPS_HIP3_ALLOWLIST_MARKETS" - "MM_PERPS_HIP3_BLOCKLIST_MARKETS" - "MM_PERPS_HIP3_ENABLED" - "MM_SECURITY_ALERTS_API_ENABLED" - "BRIDGE_USE_DEV_APIS" - "SEEDLESS_ONBOARDING_ENABLED" - "RAMP_INTERNAL_BUILD" - "FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN" - "FEATURES_ANNOUNCEMENTS_SPACE_ID" - "SEGMENT_WRITE_KEY" - "SEGMENT_PROXY_URL" - "SEGMENT_DELETE_API_SOURCE_ID" - "SEGMENT_REGULATIONS_ENDPOINT" - "MM_SENTRY_DSN" - "MM_SENTRY_AUTH_TOKEN" - "IOS_GOOGLE_CLIENT_ID" - "IOS_GOOGLE_REDIRECT_URI" - "ANDROID_APPLE_CLIENT_ID" - "ANDROID_GOOGLE_CLIENT_ID" - "ANDROID_GOOGLE_SERVER_CLIENT_ID" - "MM_INFURA_PROJECT_ID" - "MM_BRANCH_KEY_LIVE" - "MM_BRANCH_KEY_TEST" - "MM_CARD_BAANX_API_CLIENT_KEY" - "WALLET_CONNECT_PROJECT_ID" - "MM_FOX_CODE" - "FCM_CONFIG_API_KEY" - "FCM_CONFIG_AUTH_DOMAIN" - "FCM_CONFIG_STORAGE_BUCKET" - "FCM_CONFIG_PROJECT_ID" - "FCM_CONFIG_MESSAGING_SENDER_ID" - "FCM_CONFIG_APP_ID" - "FCM_CONFIG_MEASUREMENT_ID" - "QUICKNODE_MAINNET_URL" - "QUICKNODE_ARBITRUM_URL" - "QUICKNODE_AVALANCHE_URL" - "QUICKNODE_BASE_URL" - "QUICKNODE_LINEA_MAINNET_URL" - "QUICKNODE_MONAD_URL" - "QUICKNODE_OPTIMISM_URL" - "QUICKNODE_POLYGON_URL" - ) - - # Create .env file - > .env - - # Export to GITHUB_ENV if in CI environment - local exported_count=0 - for var in "${ENV_VARS[@]}"; do - # Check if variable is set (defined), not just non-empty - # This allows explicitly empty strings (e.g., MM_PERPS_HIP3_ALLOWLIST_MARKETS='') - # to be written to .env, which is semantically different from undefined variables - if [ -n "${!var+x}" ]; then - value="${!var}" - # Use double quotes with proper escaping (consistent with .js.env format) - # Escape special characters to prevent shell interpretation when sourcing - escaped_value="${value//\\/\\\\}" # Escape backslashes first - escaped_value="${escaped_value//\"/\\\"}" # Escape double quotes - escaped_value="${escaped_value//\$/\\\$}" # Escape dollar signs to prevent variable expansion - - echo "${var}=\"${escaped_value}\"" >> .env - - # Export to GITHUB_ENV if in GitHub Actions - # Note: GITHUB_ENV expects NAME=value format without quotes - if [ -n "$GITHUB_ENV" ]; then - echo "${var}=${value}" >> "$GITHUB_ENV" - fi - - ((exported_count++)) - fi - done - - echo "📄 .env file created with ${exported_count} variables" -} - # Mapping for Main env variables in the dev environment remapMainDevEnvVariables() { echo "Remapping Main target environment variables for the dev environment" @@ -699,39 +612,112 @@ generateAndroidBinary() { cd .. } -buildExpoUpdate() { - echo "Build Expo Update $METAMASK_BUILD_TYPE started..." - - # Create .env file from environment variables because Expo updates pulls env variables from .env - # see https://docs.expo.dev/eas/environment-variables/usage/#using-environment-variables-with-eas-update - createEnvFile - - # Verify .env file was created and source it - if [ -f ".env" ]; then - echo "✅ .env file exists at $(pwd)/.env" - echo "📊 .env file contains $(wc -l < .env | tr -d ' ') lines" - # Show first few variables (without values for security) - echo "📝 Sample variables in .env:" - head -n 5 .env | cut -d= -f1 | sed 's/^/ - /' +createEnvFile() { + echo "📝 Creating .env file from environment variables..." + + # List of environment variable names to export + local ENV_VARS=( + "MM_MUSD_CONVERSION_FLOW_ENABLED" + "MM_NETWORK_UI_REDESIGN_ENABLED" + "MM_NOTIFICATIONS_UI_ENABLED" + "MM_PERMISSIONS_SETTINGS_V1_ENABLED" + "MM_PERPS_BLOCKED_REGIONS" + "MM_PERPS_ENABLED" + "MM_PERPS_HIP3_ALLOWLIST_MARKETS" + "MM_PERPS_HIP3_BLOCKLIST_MARKETS" + "MM_PERPS_HIP3_ENABLED" + "MM_SECURITY_ALERTS_API_ENABLED" + "BRIDGE_USE_DEV_APIS" + "SEEDLESS_ONBOARDING_ENABLED" + "RAMP_INTERNAL_BUILD" + "FEATURES_ANNOUNCEMENTS_ACCESS_TOKEN" + "FEATURES_ANNOUNCEMENTS_SPACE_ID" + "SEGMENT_WRITE_KEY" + "SEGMENT_PROXY_URL" + "SEGMENT_DELETE_API_SOURCE_ID" + "SEGMENT_REGULATIONS_ENDPOINT" + "MM_SENTRY_DSN" + "MM_SENTRY_AUTH_TOKEN" + "IOS_GOOGLE_CLIENT_ID" + "IOS_GOOGLE_REDIRECT_URI" + "ANDROID_APPLE_CLIENT_ID" + "ANDROID_GOOGLE_CLIENT_ID" + "ANDROID_GOOGLE_SERVER_CLIENT_ID" + "MM_INFURA_PROJECT_ID" + "MM_BRANCH_KEY_LIVE" + "MM_BRANCH_KEY_TEST" + "MM_CARD_BAANX_API_CLIENT_KEY" + "WALLET_CONNECT_PROJECT_ID" + "MM_FOX_CODE" + "FCM_CONFIG_API_KEY" + "FCM_CONFIG_AUTH_DOMAIN" + "FCM_CONFIG_STORAGE_BUCKET" + "FCM_CONFIG_PROJECT_ID" + "FCM_CONFIG_MESSAGING_SENDER_ID" + "FCM_CONFIG_APP_ID" + "FCM_CONFIG_MEASUREMENT_ID" + "QUICKNODE_MAINNET_URL" + "QUICKNODE_ARBITRUM_URL" + "QUICKNODE_AVALANCHE_URL" + "QUICKNODE_BASE_URL" + "QUICKNODE_LINEA_MAINNET_URL" + "QUICKNODE_MONAD_URL" + "QUICKNODE_OPTIMISM_URL" + "QUICKNODE_POLYGON_URL" + ) + + # Create .env file and export to GITHUB_ENV + > .env + local exported_count=0 + for var in "${ENV_VARS[@]}"; do + # Check if variable is set (defined), not just non-empty + # This allows explicitly empty strings to be written to .env + if [ -n "${!var+x}" ]; then + value="${!var}" + echo "${var}=${value}" >> .env - # Source the .env file to ensure variables are loaded - echo "🔄 Sourcing .env file to load variables..." - set -a # automatically export all variables - source .env - set +a # turn off automatic export - echo "✅ .env file sourced successfully" - else - echo "⚠️ WARNING: .env file was not created!" + # Export to GITHUB_ENV if running in GitHub Actions + if [ -n "${GITHUB_ENV:-}" ]; then + echo "${var}=${value}" >> "$GITHUB_ENV" + fi + + exported_count=$((exported_count + 1)) fi + done - # Validate required Expo Update environment variables + echo "📄 .env file created with ${exported_count} variables" +} + +buildExpoUpdate() { + echo "Build Expo Update $METAMASK_BUILD_TYPE started..." + + # Create .env file and export environment variables + createEnvFile + + # Verify .env file was created and source it + if [ -f ".env" ]; then + echo "✅ .env file exists at $(pwd)/.env" + echo "📊 .env file contains $(wc -l < .env | tr -d ' ') lines" + # Show first few variables (without values for security) + echo "📝 Sample variables in .env:" + head -n 5 .env | cut -d= -f1 | sed 's/^/ - /' + + # Source the .env file to ensure variables are loaded + echo "🔄 Sourcing .env file to load variables..." + set -a # automatically export all variables + source .env + set +a # turn off automatic export + echo "✅ .env file sourced successfully" + else + echo "⚠️ WARNING: .env file was not created!" + fi + if [ -z "${EXPO_TOKEN}" ]; then echo "::error title=Missing EXPO_TOKEN::EXPO_TOKEN secret is not configured. Cannot authenticate with Expo." >&2 exit 1 - else - echo "EXPO_TOKEN is set in build.sh env (value masked by GitHub Actions logs)" fi + # Validate required Expo Update environment variables if [ -z "${EXPO_CHANNEL}" ]; then echo "::error title=Missing EXPO_CHANNEL::EXPO_CHANNEL environment variable is not set. Cannot publish update." >&2 exit 1 @@ -742,33 +728,78 @@ buildExpoUpdate() { exit 1 fi - # Prepare Expo update signing key - mkdir -p keys - echo "Writing Expo private key to ./keys/private-key.pem" - printf '%s' "${EXPO_KEY_PRIV}" > keys/private-key.pem + # Prepare Expo update signing key + mkdir -p keys + echo "Writing Expo private key to ./keys/private-key.pem" + printf '%s' "${EXPO_KEY_PRIV}" > keys/private-key.pem - if [ ! -f keys/private-key.pem ]; then - echo "::error title=Missing signing key::keys/private-key.pem not found. Ensure the signing key step ran successfully." >&2 - exit 1 - fi + if [ ! -f keys/private-key.pem ]; then + echo "::error title=Missing signing key::keys/private-key.pem not found. Ensure the signing key step ran successfully." >&2 + exit 1 + fi - echo "🚀 Publishing EAS update..." + echo "🚀 Publishing EAS update..." - echo "ℹ️ Git head: $(git rev-parse HEAD)" - echo "ℹ️ Checking for eas script in package.json..." - if ! grep -q '"eas": "eas"' package.json; then - echo "::error title=Missing eas script::package.json does not include an \"eas\" script. Commit hash: $(git rev-parse HEAD)." >&2 - exit 1 - fi + echo "ℹ️ Git head: $(git rev-parse HEAD)" + echo "ℹ️ Checking for eas script in package.json..." + if ! grep -q '"eas": "eas"' package.json; then + echo "::error title=Missing eas script::package.json does not include an \"eas\" script. Commit hash: $(git rev-parse HEAD)." >&2 + exit 1 + fi - echo "ℹ️ Available yarn scripts containing eas:" - yarn run --json | grep '"name":"eas"' || true + echo "ℹ️ Available yarn scripts containing eas:" + yarn run --json | grep '"name":"eas"' || true + # Run platforms based on OTA_PUSH_PLATFORM environment variable (default: all) + # Run sequentially to avoid LavaMoat lockdown serializer conflicts + # when bundling multiple platforms simultaneously + OTA_PUSH_PLATFORM="${OTA_PUSH_PLATFORM:-all}" + + # Track exit codes to ensure failures propagate + local ios_exit_code=0 + local android_exit_code=0 + + if [ "$OTA_PUSH_PLATFORM" = "all" ] || [ "$OTA_PUSH_PLATFORM" = "ios" ]; then + echo "📱 Publishing iOS update..." yarn run eas update \ + --platform ios \ --channel "${EXPO_CHANNEL}" \ --private-key-path "./keys/private-key.pem" \ --message "${UPDATE_MESSAGE}" \ --non-interactive + ios_exit_code=$? + + if [ $ios_exit_code -ne 0 ]; then + echo "::error title=iOS update failed::iOS EAS update command failed with exit code ${ios_exit_code}" >&2 + fi + fi + + if [ "$OTA_PUSH_PLATFORM" = "all" ] || [ "$OTA_PUSH_PLATFORM" = "android" ]; then + echo "🤖 Publishing Android update..." + yarn run eas update \ + --platform android \ + --channel "${EXPO_CHANNEL}" \ + --private-key-path "./keys/private-key.pem" \ + --message "${UPDATE_MESSAGE}" \ + --non-interactive + android_exit_code=$? + + if [ $android_exit_code -ne 0 ]; then + echo "::error title=Android update failed::Android EAS update command failed with exit code ${android_exit_code}" >&2 + fi + fi + + # Check for failures and exit accordingly + if [ $ios_exit_code -ne 0 ] || [ $android_exit_code -ne 0 ]; then + echo "::error title=EAS update failed::One or more platform updates failed. iOS exit code: ${ios_exit_code}, Android exit code: ${android_exit_code}" >&2 + exit 1 + fi + + if [ "$OTA_PUSH_PLATFORM" = "all" ]; then + echo "✅ EAS updates published for both platforms" + else + echo "✅ EAS update published for ${OTA_PUSH_PLATFORM}" + fi } buildAndroid() { @@ -872,7 +903,8 @@ checkAuthToken() { local propertiesFileName="$1" if [ -n "${MM_SENTRY_AUTH_TOKEN}" ]; then - sed -i'' -e "s/auth.token.*/auth.token=${MM_SENTRY_AUTH_TOKEN}/" "./${propertiesFileName}"; + # Use | as delimiter to avoid conflicts with special characters in auth token (e.g., /) + sed -i'' -e "s|auth.token.*|auth.token=${MM_SENTRY_AUTH_TOKEN}|" "./${propertiesFileName}"; elif ! grep -qE '^auth.token=[[:alnum:]]+$' "./${propertiesFileName}"; then if [ "$METAMASK_ENVIRONMENT" == "production" ]; then printError "Missing auth token in '${propertiesFileName}'; add the token, or set it as MM_SENTRY_AUTH_TOKEN" @@ -885,7 +917,8 @@ checkAuthToken() { if [ ! -e "./${propertiesFileName}" ]; then if [ -n "${MM_SENTRY_AUTH_TOKEN}" ]; then cp "./${propertiesFileName}.example" "./${propertiesFileName}" - sed -i'' -e "s/auth.token.*/auth.token=${MM_SENTRY_AUTH_TOKEN}/" "./${propertiesFileName}"; + # Use | as delimiter to avoid conflicts with special characters in auth token (e.g., /) + sed -i'' -e "s|auth.token.*|auth.token=${MM_SENTRY_AUTH_TOKEN}|" "./${propertiesFileName}"; else if [ "$METAMASK_ENVIRONMENT" == "production" ]; then printError "Missing '${propertiesFileName}' file (see '${propertiesFileName}.example' or set MM_SENTRY_AUTH_TOKEN to generate)" @@ -978,6 +1011,7 @@ elif [ "$PLATFORM" == "android" ]; then envFileMissing $ANDROID_ENV_FILE fi elif [ "$PLATFORM" == "expo-update" ]; then + # we don't care about env file in CI buildExpoUpdate elif [ "$PLATFORM" == "watcher" ]; then startWatcher diff --git a/scripts/set-secrets-from-config.js b/scripts/set-secrets-from-config.js new file mode 100755 index 00000000000..48c42d835c3 --- /dev/null +++ b/scripts/set-secrets-from-config.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node +/** + * Maps GitHub Secrets to environment variables based on builds.yml config. + * Called in CI after loading CONFIG_SECRETS from the build config. + * + * CONFIG_SECRETS format: { "ENV_VAR_NAME": "GITHUB_SECRET_NAME", ... } + */ + +const secretsMapping = JSON.parse(process.env.CONFIG_SECRETS || '{}'); + +Object.entries(secretsMapping).forEach(([envVar, secretName]) => { + const value = process.env[secretName]; + if (value) { + process.env[envVar] = value; + console.log(`✓ ${envVar}`); + } else { + console.warn(`⚠ ${secretName} not found (for ${envVar})`); + } +}); diff --git a/scripts/validate-build-config.js b/scripts/validate-build-config.js new file mode 100755 index 00000000000..06276130b44 --- /dev/null +++ b/scripts/validate-build-config.js @@ -0,0 +1,65 @@ +#!/usr/bin/env node +/** + * Validates builds.yml structure. Runs in CI before builds. + */ + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const BUILDS_PATH = path.join(__dirname, '../.github/builds.yml'); + +function validate() { + if (!fs.existsSync(BUILDS_PATH)) { + console.error('❌ .github/builds.yml not found'); + process.exit(1); + } + + let config; + try { + config = yaml.load(fs.readFileSync(BUILDS_PATH, 'utf8')); + } catch (e) { + console.error(`❌ Invalid YAML: ${e.message}`); + process.exit(1); + } + + if (!config.builds || typeof config.builds !== 'object') { + console.error('❌ builds.yml must have a "builds" section'); + process.exit(1); + } + + const errors = []; + const buildNames = Object.keys(config.builds); + + buildNames.forEach((name) => { + const build = config.builds[name]; + + // Required: env with METAMASK_ENVIRONMENT and METAMASK_BUILD_TYPE + if (!build.env?.METAMASK_ENVIRONMENT) { + errors.push(`${name}: missing env.METAMASK_ENVIRONMENT`); + } + if (!build.env?.METAMASK_BUILD_TYPE) { + errors.push(`${name}: missing env.METAMASK_BUILD_TYPE`); + } + + // Required: github_environment + if (!build.github_environment) { + errors.push(`${name}: missing github_environment`); + } + }); + + if (errors.length > 0) { + console.error('❌ Validation errors:'); + errors.forEach((e) => console.error(` - ${e}`)); + process.exit(1); + } + + console.log(`✅ Valid: ${buildNames.length} builds configured`); + console.log(` ${buildNames.join(', ')}`); +} + +if (require.main === module) { + validate(); +} + +module.exports = { validate }; diff --git a/scripts/verify-build-config.js b/scripts/verify-build-config.js new file mode 100644 index 00000000000..5341d3ca5d5 --- /dev/null +++ b/scripts/verify-build-config.js @@ -0,0 +1,453 @@ +#!/usr/bin/env node +/** + * Phase 1.5: Parallel Validation Script + * + * Verifies that builds.yml configuration matches the environment variables + * set by the existing Bitrise remapping functions. + * + * Run AFTER Bitrise's old remapping sets env vars, BEFORE the actual build. + * This validates builds.yml produces the same config as the legacy system. + * + * Usage: + * node scripts/verify-build-config.js + * node scripts/verify-build-config.js --strict # Exit with error on mismatch + * node scripts/verify-build-config.js --verbose # Show all comparisons + * + * Expected env vars (set by Bitrise before this script runs): + * METAMASK_BUILD_TYPE: "main" or "flask" + * METAMASK_ENVIRONMENT: "production", "rc", "test", "e2e", "exp", "dev" + */ + +const fs = require('fs'); +const path = require('path'); +const yaml = require('js-yaml'); + +const BUILDS_PATH = path.join(__dirname, '../.github/builds.yml'); +const BITRISE_PATH = path.join(__dirname, '../bitrise.yml'); + +// Environment variables to verify (env section of builds.yml) +const ENV_VARS_TO_VERIFY = [ + // Core build identity + 'METAMASK_ENVIRONMENT', + 'METAMASK_BUILD_TYPE', + // Server URLs + 'PORTFOLIO_API_URL', + 'SECURITY_ALERTS_API_URL', + 'DECODING_API_URL', + 'AUTH_SERVICE_URL', + 'REWARDS_API_URL', + 'BAANX_API_URL', + 'RAMPS_ENVIRONMENT', + // Build flags + 'BRIDGE_USE_DEV_APIS', + 'RAMP_INTERNAL_BUILD', + 'IS_TEST', + // Test/E2E specific + 'IGNORE_BOXLOGS_DEVELOPMENT', + 'IS_SIM_BUILD', + // Dev/Exp specific + 'MM_ENABLE_SETTINGS_PAGE_DEV_OPTIONS', +]; + +// Secret mappings to verify exist (secrets section of builds.yml) +// These are remapped by build.sh in Bitrise +const SECRETS_TO_VERIFY = [ + // Analytics (remapped per environment) + 'SEGMENT_WRITE_KEY', + 'SEGMENT_PROXY_URL', + 'SEGMENT_DELETE_API_SOURCE_ID', + 'SEGMENT_REGULATIONS_ENDPOINT', + // Infrastructure + 'MM_SENTRY_DSN', + 'MM_SENTRY_AUTH_TOKEN', + 'MM_INFURA_PROJECT_ID', + 'WALLET_CONNECT_PROJECT_ID', + // OAuth (remapped per environment) + 'IOS_GOOGLE_CLIENT_ID', + 'IOS_GOOGLE_REDIRECT_URI', + 'ANDROID_GOOGLE_CLIENT_ID', + 'ANDROID_APPLE_CLIENT_ID', + 'ANDROID_GOOGLE_SERVER_CLIENT_ID', + // Card/Baanx + 'MM_CARD_BAANX_API_CLIENT_KEY', + // Other critical secrets + 'MM_FOX_CODE', + 'MM_BRANCH_KEY_LIVE', + 'GOOGLE_SERVICES_B64_IOS', + 'GOOGLE_SERVICES_B64_ANDROID', +]; + +// Expected code fencing features per build type +const EXPECTED_CODE_FENCING = { + main: [ + 'preinstalled-snaps', + 'keyring-snaps', + 'multi-srp', + 'solana', + 'bitcoin', + 'tron', + ], + flask: [ + 'flask', + 'preinstalled-snaps', + 'external-snaps', + 'keyring-snaps', + 'multi-srp', + 'solana', + 'bitcoin', + 'tron', + ], +}; + +/** + * Parse bitrise.yml and extract all env var names from app.envs section + * This gives us the definitive list of what Bitrise explicitly defines + */ +function getBitriseAppEnvVars() { + if (!fs.existsSync(BITRISE_PATH)) { + console.warn(' ⚠️ bitrise.yml not found, skipping Bitrise env var check'); + return []; + } + + try { + const bitriseConfig = yaml.load(fs.readFileSync(BITRISE_PATH, 'utf8')); + const appEnvs = bitriseConfig?.app?.envs || []; + + // Extract env var names from the array of objects + // Each item is like: { opts: {...}, MM_PERPS_ENABLED: true } + const envVarNames = []; + appEnvs.forEach((envItem) => { + Object.keys(envItem).forEach((key) => { + // Skip 'opts' which is metadata + if (key !== 'opts') { + envVarNames.push(key); + } + }); + }); + + return envVarNames; + } catch (error) { + console.warn(` ⚠️ Failed to parse bitrise.yml: ${error.message}`); + return []; + } +} + +/** + * Map old Bitrise environment names to builds.yml build names + */ +function getBuildName(buildType, environment) { + // Normalize environment names + const envMap = { + production: 'prod', + development: 'dev', + // These stay the same: rc, test, e2e, exp, dev + }; + + const normalizedEnv = envMap[environment] || environment; + return `${buildType}-${normalizedEnv}`; +} + +/** + * Load config from builds.yml + */ +function loadConfig(buildName) { + if (!fs.existsSync(BUILDS_PATH)) { + throw new Error('.github/builds.yml not found'); + } + + const config = yaml.load(fs.readFileSync(BUILDS_PATH, 'utf8')); + + if (!config.builds || !config.builds[buildName]) { + const available = Object.keys(config.builds || {}).join(', '); + throw new Error(`Build "${buildName}" not found. Available: ${available}`); + } + + return config.builds[buildName]; +} + +/** + * Compare current env vars with builds.yml config + */ +function verifyConfig(options = {}) { + const { strict = false, verbose = false } = options; + + const buildType = process.env.METAMASK_BUILD_TYPE; + const environment = process.env.METAMASK_ENVIRONMENT; + + if (!buildType || !environment) { + console.error( + '❌ METAMASK_BUILD_TYPE or METAMASK_ENVIRONMENT not set in environment', + ); + console.error( + ' This script should run AFTER Bitrise remapping sets these variables.', + ); + return { success: false, mismatches: [], warnings: [] }; + } + + const buildName = getBuildName(buildType, environment); + console.log(`\n🔍 Verifying builds.yml config for: ${buildName}`); + console.log(` METAMASK_BUILD_TYPE: ${buildType}`); + console.log(` METAMASK_ENVIRONMENT: ${environment}\n`); + + let config; + try { + config = loadConfig(buildName); + } catch (error) { + console.error(`❌ ${error.message}`); + return { success: false, mismatches: [], warnings: [] }; + } + + const mismatches = []; + const matches = []; + const warnings = []; + + // Verify env vars + console.log('📋 Checking environment variables...'); + ENV_VARS_TO_VERIFY.forEach((key) => { + const currentValue = process.env[key]; + const configValue = config.env?.[key]; + + if (configValue === undefined) { + warnings.push({ key, reason: 'Not defined in builds.yml' }); + if (verbose) console.log(` ⚠️ ${key}: Not in builds.yml`); + return; + } + + const configValueStr = String(configValue); + + if (currentValue === undefined) { + warnings.push({ key, reason: 'Not set in current environment' }); + if (verbose) + console.log(` ⚠️ ${key}: Not in env (builds.yml: "${configValueStr}")`); + return; + } + + if (currentValue === configValueStr) { + matches.push({ key, value: currentValue }); + if (verbose) console.log(` ✅ ${key}: "${currentValue}"`); + } else { + mismatches.push({ + key, + expected: configValueStr, + actual: currentValue, + }); + console.log(` ❌ ${key}:`); + console.log(` Current: "${currentValue}"`); + console.log(` builds.yml: "${configValueStr}"`); + } + }); + + // Verify secrets exist in config (we can't compare actual values) + console.log('\n🔐 Checking secret mappings...'); + SECRETS_TO_VERIFY.forEach((key) => { + const secretMapping = config.secrets?.[key]; + + if (secretMapping === undefined) { + warnings.push({ key, reason: 'Secret not mapped in builds.yml' }); + console.log(` ⚠️ ${key}: Not mapped in builds.yml secrets`); + } else { + matches.push({ key, mapping: secretMapping }); + if (verbose) console.log(` ✅ ${key} → ${secretMapping}`); + } + }); + + // Verify code fencing + console.log('\n🏗️ Checking code fencing...'); + if (config.code_fencing) { + const configFeatures = config.code_fencing; + const expectedFeatures = EXPECTED_CODE_FENCING[buildType]; + + if (expectedFeatures) { + // Check if all expected features are present + const missingFeatures = expectedFeatures.filter( + (f) => !configFeatures.includes(f), + ); + const extraFeatures = configFeatures.filter( + (f) => !expectedFeatures.includes(f), + ); + + if (missingFeatures.length === 0 && extraFeatures.length === 0) { + matches.push({ key: 'code_fencing', features: configFeatures }); + if (verbose) { + console.log(` ✅ Features: ${configFeatures.join(', ')}`); + } else { + console.log( + ` ✅ ${configFeatures.length} features match expected for ${buildType}`, + ); + } + } else { + if (missingFeatures.length > 0) { + mismatches.push({ + key: 'code_fencing_missing', + expected: missingFeatures.join(', '), + actual: 'not present', + }); + console.log(` ❌ Missing features: ${missingFeatures.join(', ')}`); + } + if (extraFeatures.length > 0) { + warnings.push({ + key: 'code_fencing_extra', + reason: `Extra features: ${extraFeatures.join(', ')}`, + }); + console.log(` ⚠️ Extra features: ${extraFeatures.join(', ')}`); + } + } + } else { + warnings.push({ + key: 'code_fencing', + reason: `Unknown build type: ${buildType}`, + }); + console.log(` ⚠️ Unknown build type "${buildType}" for code fencing check`); + if (verbose) { + console.log(` Features: ${configFeatures.join(', ')}`); + } + } + } else { + mismatches.push({ + key: 'code_fencing', + expected: 'defined', + actual: 'undefined', + }); + console.log(` ❌ No code fencing defined in builds.yml`); + } + + // Verify remote feature flags + console.log('\n🚩 Checking remote feature flags...'); + if (config.remote_feature_flags) { + const flags = Object.keys(config.remote_feature_flags); + if (verbose) { + Object.entries(config.remote_feature_flags).forEach(([flag, value]) => { + console.log(` ✅ ${flag}: ${value}`); + }); + } else { + console.log(` ✅ ${flags.length} remote feature flags configured`); + } + } else { + warnings.push({ + key: 'remote_feature_flags', + reason: 'Not defined in builds.yml', + }); + console.log(` ⚠️ No remote feature flags defined`); + } + + // REVERSE CHECK: Find Bitrise env vars NOT in builds.yml + console.log('\n🔄 Checking for Bitrise env vars NOT in builds.yml...'); + + // Get all keys from builds.yml (env + secrets) + const buildsYmlKeys = new Set([ + ...Object.keys(config.env || {}), + ...Object.keys(config.secrets || {}), + ]); + + // Get explicit env vars defined in bitrise.yml app.envs section + const bitriseDefinedEnvVars = getBitriseAppEnvVars(); + + if (bitriseDefinedEnvVars.length === 0) { + console.log(' ℹ️ Could not parse bitrise.yml env vars'); + } else { + // Find env vars defined in bitrise.yml but not in builds.yml + const missingFromBuildsYml = bitriseDefinedEnvVars.filter( + (key) => !buildsYmlKeys.has(key), + ); + const accountedFor = bitriseDefinedEnvVars.filter((key) => + buildsYmlKeys.has(key), + ); + + if (missingFromBuildsYml.length > 0) { + console.log( + ` ⚠️ ${missingFromBuildsYml.length} bitrise.yml env vars NOT in builds.yml:`, + ); + missingFromBuildsYml.sort().forEach((key) => { + // Get current value from process.env and mask for security + const value = process.env[key]; + let displayValue; + if (value === undefined) { + displayValue = '[not set in env]'; + } else if (value.length > 6) { + displayValue = `${value.substring(0, 3)}...${value.substring(value.length - 3)}`; + } else { + displayValue = '[SET]'; + } + warnings.push({ + key, + reason: 'Defined in bitrise.yml but missing from builds.yml', + }); + console.log(` - ${key}: ${displayValue}`); + }); + } + + if (accountedFor.length > 0) { + if (verbose) { + console.log( + ` ✅ ${accountedFor.length} bitrise.yml env vars found in builds.yml:`, + ); + accountedFor.sort().forEach((key) => { + console.log(` - ${key}`); + }); + } else { + console.log( + ` ✅ ${accountedFor.length} bitrise.yml env vars accounted for in builds.yml`, + ); + } + } + + if (missingFromBuildsYml.length === 0 && accountedFor.length === 0) { + console.log( + ` ℹ️ No overlap between bitrise.yml app.envs (${bitriseDefinedEnvVars.length} vars) and builds.yml`, + ); + } + + // Summary of bitrise.yml parsing + if (verbose) { + console.log( + `\n 📊 bitrise.yml app.envs total: ${bitriseDefinedEnvVars.length} env vars`, + ); + } + } + + // Summary + console.log('\n' + '─'.repeat(60)); + if (mismatches.length === 0) { + console.log('✅ Config verification PASSED'); + console.log(` ${matches.length} items matched`); + if (warnings.length > 0) { + console.log(` ${warnings.length} warnings (non-critical)`); + } + } else { + console.log('❌ Config verification FAILED'); + console.log(` ${mismatches.length} mismatches found`); + console.log(` ${matches.length} items matched`); + if (warnings.length > 0) { + console.log(` ${warnings.length} warnings`); + } + } + console.log('─'.repeat(60) + '\n'); + + const success = mismatches.length === 0; + + if (strict && !success) { + console.error( + '🛑 Strict mode: Exiting with error due to config mismatches', + ); + console.error( + ' Fix builds.yml to match current Bitrise configuration, or update Bitrise.', + ); + } + + return { success, mismatches, warnings, matches }; +} + +// CLI +if (require.main === module) { + const args = process.argv.slice(2); + const strict = args.includes('--strict'); + const verbose = args.includes('--verbose'); + + const result = verifyConfig({ strict, verbose }); + + if (strict && !result.success) { + process.exit(1); + } +} + +module.exports = { verifyConfig, getBuildName, loadConfig }; diff --git a/e2e/specs/multisrp/utils.ts b/tests/flows/accounts.flow.ts similarity index 66% rename from e2e/specs/multisrp/utils.ts rename to tests/flows/accounts.flow.ts index 5b37f0c57c7..632d2760e25 100644 --- a/e2e/specs/multisrp/utils.ts +++ b/tests/flows/accounts.flow.ts @@ -1,16 +1,16 @@ -import ImportSrpView from '../../pages/importSrp/ImportSrpView'; -import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet'; -import AddAccountBottomSheet from '../../pages/wallet/AddAccountBottomSheet'; -import WalletView from '../../pages/wallet/WalletView'; -import Assertions from '../../../tests/framework/Assertions.ts'; -import SRPListItemComponent from '../../pages/wallet/MultiSrp/Common/SRPListItemComponent'; -import SrpQuizModal from '../../pages/Settings/SecurityAndPrivacy/SrpQuizModal'; -import RevealSecretRecoveryPhrase from '../../pages/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase'; -import { RevealSeedViewSelectorsText } from '../../../app/components/Views/RevealPrivateCredential/RevealSeedView.testIds'; -import TabBarComponent from '../../pages/wallet/TabBarComponent'; -import SettingsView from '../../pages/Settings/SettingsView'; -import SecurityAndPrivacyView from '../../pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView'; -import AccountDetails from '../../pages/MultichainAccounts/AccountDetails.ts'; +import ImportSrpView from '../../e2e/pages/importSrp/ImportSrpView.ts'; +import AccountListBottomSheet from '../../e2e/pages/wallet/AccountListBottomSheet.ts'; +import AddAccountBottomSheet from '../../e2e/pages/wallet/AddAccountBottomSheet.ts'; +import WalletView from '../../e2e/pages/wallet/WalletView.ts'; +import Assertions from '../framework/Assertions.ts'; +import SRPListItemComponent from '../../e2e/pages/wallet/MultiSrp/Common/SRPListItemComponent.ts'; +import SrpQuizModal from '../../e2e/pages/Settings/SecurityAndPrivacy/SrpQuizModal.ts'; +import RevealSecretRecoveryPhrase from '../../e2e/pages/Settings/SecurityAndPrivacy/RevealSecretRecoveryPhrase.ts'; +import { RevealSeedViewSelectorsText } from '../../app/components/Views/RevealPrivateCredential/RevealSeedView.testIds.ts'; +import TabBarComponent from '../../e2e/pages/wallet/TabBarComponent.ts'; +import SettingsView from '../../e2e/pages/Settings/SettingsView.ts'; +import SecurityAndPrivacyView from '../../e2e/pages/Settings/SecurityAndPrivacy/SecurityAndPrivacyView.ts'; +import AccountDetails from '../../e2e/pages/MultichainAccounts/AccountDetails.ts'; const PASSWORD = '123123123'; diff --git a/tests/page-objects/Trending/TrendingView.ts b/tests/page-objects/Trending/TrendingView.ts index bae25f41150..c183fa8948a 100644 --- a/tests/page-objects/Trending/TrendingView.ts +++ b/tests/page-objects/Trending/TrendingView.ts @@ -231,7 +231,7 @@ class TrendingView { await Gestures.tap(backButton, { elemDescription: `Tap Back Button from ${sectionTitle} Full View`, - checkStability: true, + checkVisibility: false, }); } diff --git a/e2e/specs/assets/asset-sort.spec.ts b/tests/regression/assets/asset-sort.spec.ts similarity index 85% rename from e2e/specs/assets/asset-sort.spec.ts rename to tests/regression/assets/asset-sort.spec.ts index 6cff90ffa61..9bbab4dd576 100644 --- a/e2e/specs/assets/asset-sort.spec.ts +++ b/tests/regression/assets/asset-sort.spec.ts @@ -1,15 +1,15 @@ -import { RegressionAssets } from '../../tags'; -import WalletView from '../../pages/wallet/WalletView'; -import SortModal from '../../pages/wallet/TokenSortBottomSheet'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { loginToApp } from '../../viewHelper'; -import Assertions from '../../../tests/framework/Assertions'; -import ConfirmAddAssetView from '../../pages/wallet/ImportTokenFlow/ConfirmAddAsset'; -import ImportTokensView from '../../pages/wallet/ImportTokenFlow/ImportTokensView'; -import { MockApiEndpoint } from '../../../tests/framework'; +import { RegressionAssets } from '../../../e2e/tags'; +import WalletView from '../../../e2e/pages/wallet/WalletView'; +import SortModal from '../../../e2e/pages/wallet/TokenSortBottomSheet'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; +import { loginToApp } from '../../../e2e/viewHelper'; +import Assertions from '../../framework/Assertions'; +import ConfirmAddAssetView from '../../../e2e/pages/wallet/ImportTokenFlow/ConfirmAddAsset'; +import ImportTokensView from '../../../e2e/pages/wallet/ImportTokenFlow/ImportTokensView'; +import { MockApiEndpoint } from '../../framework'; import { Mockttp } from 'mockttp'; -import { setupMockRequest } from '../../../tests/api-mocking/helpers/mockHelpers'; +import { setupMockRequest } from '../../api-mocking/helpers/mockHelpers'; const AAVE_MAINNET_DETAILS = { address: '0x7Fc66500c84A76Ad7e9c93437bFc5Ac33E2DDaE9', diff --git a/e2e/specs/assets/defi/view-defi-tab.spec.ts b/tests/regression/assets/defi/view-defi-tab.spec.ts similarity index 88% rename from e2e/specs/assets/defi/view-defi-tab.spec.ts rename to tests/regression/assets/defi/view-defi-tab.spec.ts index ba4c626bb7f..b4a4f87a71e 100644 --- a/e2e/specs/assets/defi/view-defi-tab.spec.ts +++ b/tests/regression/assets/defi/view-defi-tab.spec.ts @@ -1,18 +1,18 @@ -import { RegressionNetworkAbstractions } from '../../../tags'; -import WalletView from '../../../pages/wallet/WalletView'; -import Assertions from '../../../../tests/framework/Assertions'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; +import { RegressionNetworkAbstractions } from '../../../../e2e/tags'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; +import Assertions from '../../../framework/Assertions'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; import { WalletViewSelectorsText } from '../../../../app/components/Views/Wallet/WalletView.testIds'; -import { loginToApp } from '../../../viewHelper'; -import { setupMockRequest } from '../../../../tests/api-mocking/helpers/mockHelpers'; +import { loginToApp } from '../../../../e2e/viewHelper'; +import { setupMockRequest } from '../../../api-mocking/helpers/mockHelpers'; import { Mockttp } from 'mockttp'; import { defiPositionsError, defiPositionsWithData, defiPositionsWithNoData, -} from '../../../../tests/api-mocking/mock-responses/defi-api-mocks'; -import NetworkManager from '../../../pages/wallet/NetworkManager'; +} from '../../../api-mocking/mock-responses/defi-api-mocks.ts'; +import NetworkManager from '../../../../e2e/pages/wallet/NetworkManager.ts'; describe(RegressionNetworkAbstractions('View DeFi tab'), () => { it('open the DeFi tab with an address that has no positions', async () => { diff --git a/e2e/specs/assets/import-custom-token.spec.ts b/tests/regression/assets/import-custom-token.spec.ts similarity index 72% rename from e2e/specs/assets/import-custom-token.spec.ts rename to tests/regression/assets/import-custom-token.spec.ts index 9b24c089384..026c579fbf1 100644 --- a/e2e/specs/assets/import-custom-token.spec.ts +++ b/tests/regression/assets/import-custom-token.spec.ts @@ -1,16 +1,16 @@ -import { RegressionAssets } from '../../tags'; -import TestHelpers from '../../helpers'; -import WalletView from '../../pages/wallet/WalletView'; -import ConfirmAddAssetView from '../../pages/wallet/ImportTokenFlow/ConfirmAddAsset'; -import ImportTokensView from '../../pages/wallet/ImportTokenFlow/ImportTokensView'; -import Assertions from '../../../tests/framework/Assertions'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { loginToApp } from '../../viewHelper'; +import { RegressionAssets } from '../../../e2e/tags'; +import TestHelpers from '../../../e2e/helpers'; +import WalletView from '../../../e2e/pages/wallet/WalletView'; +import ConfirmAddAssetView from '../../../e2e/pages/wallet/ImportTokenFlow/ConfirmAddAsset'; +import ImportTokensView from '../../../e2e/pages/wallet/ImportTokenFlow/ImportTokensView'; +import Assertions from '../../framework/Assertions'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { loginToApp } from '../../../e2e/viewHelper'; import { SMART_CONTRACTS } from '../../../app/util/test/smart-contracts'; -import { AnvilPort } from '../../../tests/framework/fixtures/FixtureUtils'; -import { LocalNode } from '../../../tests/framework/types'; -import { AnvilManager } from '../../../tests/seeder/anvil-manager'; +import { AnvilPort } from '../../framework/fixtures/FixtureUtils'; +import { LocalNode } from '../../framework'; +import { AnvilManager } from '../../seeder/anvil-manager'; describe(RegressionAssets('Import custom token'), () => { beforeAll(async () => { diff --git a/e2e/specs/assets/import-tokens-via-asset-watcher.spec.ts b/tests/regression/assets/import-tokens-via-asset-watcher.spec.ts similarity index 76% rename from e2e/specs/assets/import-tokens-via-asset-watcher.spec.ts rename to tests/regression/assets/import-tokens-via-asset-watcher.spec.ts index 87e7360501e..cc5c35d3622 100644 --- a/e2e/specs/assets/import-tokens-via-asset-watcher.spec.ts +++ b/tests/regression/assets/import-tokens-via-asset-watcher.spec.ts @@ -1,28 +1,28 @@ -import { RegressionNetworkAbstractions } from '../../tags'; -import TestHelpers from '../../helpers'; -import { loginToApp, navigateToBrowserView } from '../../viewHelper'; +import { RegressionNetworkAbstractions } from '../../../e2e/tags'; +import TestHelpers from '../../../e2e/helpers'; +import { loginToApp, navigateToBrowserView } from '../../../e2e/viewHelper'; import FixtureBuilder, { DEFAULT_FIXTURE_ACCOUNT, -} from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; +} from '../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; import { SMART_CONTRACTS } from '../../../app/util/test/smart-contracts'; -import TabBarComponent from '../../pages/wallet/TabBarComponent'; -import TestDApp from '../../pages/Browser/TestDApp'; -import Assertions from '../../../tests/framework/Assertions'; -import AssetWatchBottomSheet from '../../pages/Transactions/AssetWatchBottomSheet'; -import WalletView from '../../pages/wallet/WalletView'; -import NetworkListModal from '../../pages/Network/NetworkListModal'; +import TabBarComponent from '../../../e2e/pages/wallet/TabBarComponent'; +import TestDApp from '../../../e2e/pages/Browser/TestDApp'; +import Assertions from '../../framework/Assertions'; +import AssetWatchBottomSheet from '../../../e2e/pages/Transactions/AssetWatchBottomSheet'; +import WalletView from '../../../e2e/pages/wallet/WalletView'; +import NetworkListModal from '../../../e2e/pages/Network/NetworkListModal'; import { AnvilPort, buildPermissions, -} from '../../../tests/framework/fixtures/FixtureUtils'; -import { DappVariants } from '../../../tests/framework/Constants'; +} from '../../framework/fixtures/FixtureUtils'; +import { DappVariants } from '../../framework/Constants'; import { setEthAccounts, Caip25EndowmentPermissionName, } from '@metamask/chain-agnostic-permission'; -import { LocalNode } from '../../../tests/framework/types'; -import { AnvilManager } from '../../../tests/seeder/anvil-manager'; +import { LocalNode } from '../../framework'; +import { AnvilManager } from '../../seeder/anvil-manager'; const ERC20_CONTRACT = SMART_CONTRACTS.HST; diff --git a/e2e/specs/assets/import-tokens.spec.ts b/tests/regression/assets/import-tokens.spec.ts similarity index 88% rename from e2e/specs/assets/import-tokens.spec.ts rename to tests/regression/assets/import-tokens.spec.ts index 0465fe8748c..773af152d76 100644 --- a/e2e/specs/assets/import-tokens.spec.ts +++ b/tests/regression/assets/import-tokens.spec.ts @@ -1,13 +1,13 @@ -import { RegressionAssets } from '../../tags'; -import WalletView from '../../pages/wallet/WalletView'; -import ImportTokensView from '../../pages/wallet/ImportTokenFlow/ImportTokensView'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { loginToApp } from '../../viewHelper'; -import ConfirmAddAssetView from '../../pages/wallet/ImportTokenFlow/ConfirmAddAsset'; -import Assertions from '../../../tests/framework/Assertions'; +import { RegressionAssets } from '../../../e2e/tags'; +import WalletView from '../../../e2e/pages/wallet/WalletView'; +import ImportTokensView from '../../../e2e/pages/wallet/ImportTokenFlow/ImportTokensView'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; +import { loginToApp } from '../../../e2e/viewHelper'; +import ConfirmAddAssetView from '../../../e2e/pages/wallet/ImportTokenFlow/ConfirmAddAsset'; +import Assertions from '../../framework/Assertions'; import { Mockttp } from 'mockttp'; -import { setupMockRequest } from '../../../tests/api-mocking/helpers/mockHelpers'; +import { setupMockRequest } from '../../api-mocking/helpers/mockHelpers'; describe(RegressionAssets('Import Tokens'), () => { const testSpecificMock = async (mockServer: Mockttp) => { diff --git a/e2e/specs/assets/multichain/asset-list.spec.ts b/tests/regression/assets/multichain/asset-list.spec.ts similarity index 90% rename from e2e/specs/assets/multichain/asset-list.spec.ts rename to tests/regression/assets/multichain/asset-list.spec.ts index 7ae3c411914..88453afd0d1 100644 --- a/e2e/specs/assets/multichain/asset-list.spec.ts +++ b/tests/regression/assets/multichain/asset-list.spec.ts @@ -1,16 +1,16 @@ -import { RegressionAssets } from '../../../tags'; -import WalletView from '../../../pages/wallet/WalletView'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; -import { loginToApp } from '../../../viewHelper'; -import Assertions from '../../../../tests/framework/Assertions'; -import TokenOverview from '../../../pages/wallet/TokenOverview'; -import NetworkManager from '../../../pages/wallet/NetworkManager'; -import { setupRemoteFeatureFlagsMock } from '../../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; +import { RegressionAssets } from '../../../../e2e/tags'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import { loginToApp } from '../../../../e2e/viewHelper'; +import Assertions from '../../../framework/Assertions'; +import TokenOverview from '../../../../e2e/pages/wallet/TokenOverview'; +import NetworkManager from '../../../../e2e/pages/wallet/NetworkManager'; +import { setupRemoteFeatureFlagsMock } from '../../../api-mocking/helpers/remoteFeatureFlagsHelper'; import { remoteFeatureFlagTronAccounts, remoteFeatureMultichainAccountsAccountDetailsV2, -} from '../../../../tests/api-mocking/mock-responses/feature-flags-mocks'; +} from '../../../api-mocking/mock-responses/feature-flags-mocks.ts'; const ETHEREUM_NAME = 'Ethereum'; const AVAX_NAME = 'AVAX'; diff --git a/e2e/specs/assets/nft-details.spec.ts b/tests/regression/assets/nft-details.spec.ts similarity index 79% rename from e2e/specs/assets/nft-details.spec.ts rename to tests/regression/assets/nft-details.spec.ts index 7dc76cd7b09..2545b0304dd 100644 --- a/e2e/specs/assets/nft-details.spec.ts +++ b/tests/regression/assets/nft-details.spec.ts @@ -1,20 +1,20 @@ -import { RegressionAssets } from '../../tags'; -import TestHelpers from '../../helpers'; -import { loginToApp } from '../../viewHelper'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; +import { RegressionAssets } from '../../../e2e/tags'; +import TestHelpers from '../../../e2e/helpers'; +import { loginToApp } from '../../../e2e/viewHelper'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; import { SMART_CONTRACTS } from '../../../app/util/test/smart-contracts'; -import WalletView from '../../pages/wallet/WalletView'; -import ImportNFTView from '../../pages/wallet/ImportNFTFlow/ImportNFTView'; -import Assertions from '../../../tests/framework/Assertions'; +import WalletView from '../../../e2e/pages/wallet/WalletView'; +import ImportNFTView from '../../../e2e/pages/wallet/ImportNFTFlow/ImportNFTView'; +import Assertions from '../../framework/Assertions'; import enContent from '../../../locales/languages/en.json'; import { AnvilPort, buildPermissions, -} from '../../../tests/framework/fixtures/FixtureUtils'; -import { DappVariants } from '../../../tests/framework/Constants'; -import { LocalNode } from '../../../tests/framework/types'; -import { AnvilManager } from '../../../tests/seeder/anvil-manager'; +} from '../../framework/fixtures/FixtureUtils'; +import { DappVariants } from '../../framework/Constants'; +import { LocalNode } from '../../framework'; +import { AnvilManager } from '../../seeder/anvil-manager'; describe.skip(RegressionAssets('NFT Details page'), () => { const NFT_CONTRACT = SMART_CONTRACTS.NFTS; diff --git a/e2e/specs/assets/nft-detection-modal.spec.ts b/tests/regression/assets/nft-detection-modal.spec.ts similarity index 81% rename from e2e/specs/assets/nft-detection-modal.spec.ts rename to tests/regression/assets/nft-detection-modal.spec.ts index 9d205fa971e..de42c28a8a2 100644 --- a/e2e/specs/assets/nft-detection-modal.spec.ts +++ b/tests/regression/assets/nft-detection-modal.spec.ts @@ -1,13 +1,13 @@ -import WalletView from '../../pages/wallet/WalletView'; -import { loginToApp } from '../../viewHelper'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import TestHelpers from '../../helpers'; -import Assertions from '../../../tests/framework/Assertions'; -import NftDetectionModal from '../../pages/wallet/NftDetectionModal'; -import { RegressionAssets } from '../../tags'; +import WalletView from '../../../e2e/pages/wallet/WalletView'; +import { loginToApp } from '../../../e2e/viewHelper'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; +import TestHelpers from '../../../e2e/helpers'; +import Assertions from '../../framework/Assertions'; +import NftDetectionModal from '../../../e2e/pages/wallet/NftDetectionModal'; +import { RegressionAssets } from '../../../e2e/tags'; -import { NftDetectionModalSelectorsText } from '../../../app/components/Views/NFTAutoDetectionModal/NftDetectionModal.testIds'; +import { NftDetectionModalSelectorsText } from '../../../app/components/Views/NFTAutoDetectionModal/NftDetectionModal.testIds.ts'; describe.skip(RegressionAssets('NFT Detection Modal'), () => { beforeAll(async () => { diff --git a/e2e/specs/assets/token-detection-import-all.spec.ts b/tests/regression/assets/token-detection-import-all.spec.ts similarity index 72% rename from e2e/specs/assets/token-detection-import-all.spec.ts rename to tests/regression/assets/token-detection-import-all.spec.ts index f318535b662..2978741fb5a 100644 --- a/e2e/specs/assets/token-detection-import-all.spec.ts +++ b/tests/regression/assets/token-detection-import-all.spec.ts @@ -1,11 +1,11 @@ 'use strict'; -import { loginToApp } from '../../viewHelper'; -import { RegressionAssets } from '../../tags'; -import WalletView from '../../pages/wallet/WalletView'; -import Assertions from '../../../tests/framework/Assertions'; -import TestHelpers from '../../helpers'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; +import { loginToApp } from '../../../e2e/viewHelper'; +import { RegressionAssets } from '../../../e2e/tags'; +import WalletView from '../../../e2e/pages/wallet/WalletView'; +import Assertions from '../../framework/Assertions'; +import TestHelpers from '../../../e2e/helpers'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; const ETHEREUM_NAME = 'Ethereum'; const USDC_NAME = 'USDCoin'; diff --git a/e2e/specs/assets/transaction.spec.ts b/tests/regression/assets/transaction.spec.ts similarity index 68% rename from e2e/specs/assets/transaction.spec.ts rename to tests/regression/assets/transaction.spec.ts index e852df97a0f..41a6ba09225 100644 --- a/e2e/specs/assets/transaction.spec.ts +++ b/tests/regression/assets/transaction.spec.ts @@ -1,22 +1,22 @@ -import TestHelpers from '../../helpers'; -import { RegressionAssets } from '../../tags'; -import RedesignedSendView from '../../pages/Send/RedesignedSendView'; -import TransactionConfirmationView from '../../pages/Send/TransactionConfirmView'; -import { loginToApp } from '../../viewHelper'; -import TabBarComponent from '../../pages/wallet/TabBarComponent'; +import TestHelpers from '../../../e2e/helpers'; +import { RegressionAssets } from '../../../e2e/tags'; +import RedesignedSendView from '../../../e2e/pages/Send/RedesignedSendView'; +import TransactionConfirmationView from '../../../e2e/pages/Send/TransactionConfirmView'; +import { loginToApp } from '../../../e2e/viewHelper'; +import TabBarComponent from '../../../e2e/pages/wallet/TabBarComponent'; import enContent from '../../../locales/languages/en.json'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import Assertions from '../../../tests/framework/Assertions'; -import WalletView from '../../pages/wallet/WalletView'; -import TokenOverview from '../../pages/wallet/TokenOverview'; -import ToastModal from '../../pages/wallet/ToastModal'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper'; +import Assertions from '../../framework/Assertions'; +import WalletView from '../../../e2e/pages/wallet/WalletView'; +import TokenOverview from '../../../e2e/pages/wallet/TokenOverview'; +import ToastModal from '../../../e2e/pages/wallet/ToastModal'; import { Mockttp } from 'mockttp'; -import { setupRemoteFeatureFlagsMock } from '../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { confirmationFeatureFlags } from '../../../tests/api-mocking/mock-responses/feature-flags-mocks'; -import { LocalNode } from '../../../tests/framework/types'; -import { AnvilPort } from '../../../tests/framework/fixtures/FixtureUtils'; -import { AnvilManager } from '../../../tests/seeder/anvil-manager'; +import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper'; +import { confirmationFeatureFlags } from '../../api-mocking/mock-responses/feature-flags-mocks'; +import { LocalNode } from '../../framework/types'; +import { AnvilPort } from '../../framework/fixtures/FixtureUtils'; +import { AnvilManager } from '../../seeder/anvil-manager'; describe(RegressionAssets('Transaction'), () => { beforeAll(async () => { diff --git a/e2e/specs/ramps/onramp-parameters.spec.ts b/tests/regression/ramps/onramp-parameters.spec.ts similarity index 78% rename from e2e/specs/ramps/onramp-parameters.spec.ts rename to tests/regression/ramps/onramp-parameters.spec.ts index 538b5ff69cb..08bd5a82ed8 100644 --- a/e2e/specs/ramps/onramp-parameters.spec.ts +++ b/tests/regression/ramps/onramp-parameters.spec.ts @@ -1,29 +1,26 @@ -import { loginToApp } from '../../viewHelper'; -import WalletView from '../../pages/wallet/WalletView'; -import FundActionMenu from '../../pages/UI/FundActionMenu'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { CustomNetworks } from '../../../tests/resources/networks.e2e'; -import { RegressionTrade } from '../../tags'; -import Assertions from '../../../tests/framework/Assertions'; -import BuildQuoteView from '../../pages/Ramps/BuildQuoteView'; -import SelectCurrencyView from '../../pages/Ramps/SelectCurrencyView'; -import TokenSelectBottomSheet from '../../pages/Ramps/TokenSelectBottomSheet'; -import SelectRegionView from '../../pages/Ramps/SelectRegionView'; -import SelectPaymentMethodView from '../../pages/Ramps/SelectPaymentMethodView'; -import BuyGetStartedView from '../../pages/Ramps/BuyGetStartedView'; +import { loginToApp } from '../../../e2e/viewHelper.ts'; +import WalletView from '../../../e2e/pages/wallet/WalletView.ts'; +import FundActionMenu from '../../../e2e/pages/UI/FundActionMenu.ts'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper.ts'; +import { CustomNetworks } from '../../resources/networks.e2e'; +import { RegressionTrade } from '../../../e2e/tags'; +import Assertions from '../../framework/Assertions.ts'; +import BuildQuoteView from '../../../e2e/pages/Ramps/BuildQuoteView.ts'; +import SelectCurrencyView from '../../../e2e/pages/Ramps/SelectCurrencyView.ts'; +import TokenSelectBottomSheet from '../../../e2e/pages/Ramps/TokenSelectBottomSheet.ts'; +import SelectRegionView from '../../../e2e/pages/Ramps/SelectRegionView.ts'; +import SelectPaymentMethodView from '../../../e2e/pages/Ramps/SelectPaymentMethodView.ts'; +import BuyGetStartedView from '../../../e2e/pages/Ramps/BuyGetStartedView.ts'; import { EventPayload, getEventsPayloads, -} from '../../../tests/helpers/analytics/helpers'; -import SoftAssert from '../../../tests/framework/SoftAssert'; -import { - RampsRegions, - RampsRegionsEnum, -} from '../../../tests/framework/Constants'; -import Matchers from '../../../tests/framework/Matchers'; +} from '../../helpers/analytics/helpers.ts'; +import SoftAssert from '../../framework/SoftAssert.ts'; +import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants.ts'; +import Matchers from '../../framework/Matchers.ts'; import { Mockttp } from 'mockttp'; -import { setupRegionAwareOnRampMocks } from '../../../tests/api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup'; +import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup.ts'; const eventsToCheck: EventPayload[] = []; diff --git a/e2e/specs/ramps/ramps-account-switch.spec.ts b/tests/regression/ramps/ramps-account-switch.spec.ts similarity index 76% rename from e2e/specs/ramps/ramps-account-switch.spec.ts rename to tests/regression/ramps/ramps-account-switch.spec.ts index 9d67a068d10..a6db9ec678d 100644 --- a/e2e/specs/ramps/ramps-account-switch.spec.ts +++ b/tests/regression/ramps/ramps-account-switch.spec.ts @@ -1,22 +1,19 @@ -import { loginToApp } from '../../viewHelper'; -import TabBarComponent from '../../pages/wallet/TabBarComponent'; -import WalletView from '../../pages/wallet/WalletView'; -import FundActionMenu from '../../pages/UI/FundActionMenu'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { Assertions } from '../../../tests/framework'; -import BuyGetStartedView from '../../pages/Ramps/BuyGetStartedView'; -import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet'; -import BuildQuoteView from '../../pages/Ramps/BuildQuoteView'; -import { RegressionTrade } from '../../tags'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { LocalNodeType } from '../../../tests/framework/types'; -import { Hardfork } from '../../../tests/seeder/anvil-manager'; -import { - RampsRegions, - RampsRegionsEnum, -} from '../../../tests/framework/Constants'; +import { loginToApp } from '../../../e2e/viewHelper.ts'; +import TabBarComponent from '../../../e2e/pages/wallet/TabBarComponent.ts'; +import WalletView from '../../../e2e/pages/wallet/WalletView.ts'; +import FundActionMenu from '../../../e2e/pages/UI/FundActionMenu.ts'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts'; +import { Assertions } from '../../framework'; +import BuyGetStartedView from '../../../e2e/pages/Ramps/BuyGetStartedView.ts'; +import AccountListBottomSheet from '../../../e2e/pages/wallet/AccountListBottomSheet.ts'; +import BuildQuoteView from '../../../e2e/pages/Ramps/BuildQuoteView.ts'; +import { RegressionTrade } from '../../../e2e/tags'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper.ts'; +import { LocalNodeType } from '../../framework/types.ts'; +import { Hardfork } from '../../seeder/anvil-manager.ts'; +import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants.ts'; import { Mockttp } from 'mockttp'; -import { setupRegionAwareOnRampMocks } from '../../../tests/api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup'; +import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup.ts'; // Anvil configuration for local blockchain node const anvilLocalNodeOptions = { diff --git a/tests/smoke/accounts/reveal-secret-recovery-phrase.spec.ts b/tests/smoke/accounts/reveal-secret-recovery-phrase.spec.ts index 0e2a37426f5..94dc5e1bede 100644 --- a/tests/smoke/accounts/reveal-secret-recovery-phrase.spec.ts +++ b/tests/smoke/accounts/reveal-secret-recovery-phrase.spec.ts @@ -6,7 +6,7 @@ import SecurityAndPrivacy from '../../../e2e/pages/Settings/SecurityAndPrivacy/S import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; import { withFixtures } from '../../framework/fixtures/FixtureHelper'; import Assertions from '../../framework/Assertions'; -import { completeSrpQuiz } from '../../../e2e/specs/multisrp/utils'; +import { completeSrpQuiz } from '../../flows/accounts.flow.ts'; import { defaultGanacheOptions } from '../../framework/Constants'; describe(SmokeAccounts('Secret Recovery Phrase Reveal from Settings'), () => { diff --git a/tests/smoke/accounts/wallet-details.spec.ts b/tests/smoke/accounts/wallet-details.spec.ts index 5f1f835cff0..d8891898f80 100644 --- a/tests/smoke/accounts/wallet-details.spec.ts +++ b/tests/smoke/accounts/wallet-details.spec.ts @@ -4,7 +4,7 @@ import Assertions from '../../framework/Assertions'; import WalletView from '../../../e2e/pages/wallet/WalletView'; import AccountDetails from '../../../e2e/pages/MultichainAccounts/AccountDetails'; import WalletDetails from '../../../e2e/pages/MultichainAccounts/WalletDetails'; -import { completeSrpQuiz } from '../../../e2e/specs/multisrp/utils'; +import { completeSrpQuiz } from '../../flows/accounts.flow.ts'; import { defaultGanacheOptions } from '../../framework/Constants'; import FixtureBuilder from '../../framework/fixtures/FixtureBuilder'; import { withFixtures } from '../../framework/fixtures/FixtureHelper'; diff --git a/e2e/specs/assets/defi/view-defi-details.spec.ts b/tests/smoke/assets/defi/view-defi-details.spec.ts similarity index 74% rename from e2e/specs/assets/defi/view-defi-details.spec.ts rename to tests/smoke/assets/defi/view-defi-details.spec.ts index bed0cca16ec..f66830fbbd6 100644 --- a/e2e/specs/assets/defi/view-defi-details.spec.ts +++ b/tests/smoke/assets/defi/view-defi-details.spec.ts @@ -1,12 +1,12 @@ -import { SmokeNetworkAbstractions } from '../../../tags'; -import WalletView from '../../../pages/wallet/WalletView'; -import Assertions from '../../../../tests/framework/Assertions'; -import { withFixtures } from '../../../../tests/framework/fixtures/FixtureHelper'; -import FixtureBuilder from '../../../../tests/framework/fixtures/FixtureBuilder'; -import { loginToApp } from '../../../viewHelper'; +import { SmokeNetworkAbstractions } from '../../../../e2e/tags'; +import WalletView from '../../../../e2e/pages/wallet/WalletView'; +import Assertions from '../../../framework/Assertions'; +import { withFixtures } from '../../../framework/fixtures/FixtureHelper'; +import FixtureBuilder from '../../../framework/fixtures/FixtureBuilder'; +import { loginToApp } from '../../../../e2e/viewHelper'; import { Mockttp } from 'mockttp'; -import { setupMockRequest } from '../../../../tests/api-mocking/helpers/mockHelpers'; -import { defiPositionsWithData } from '../../../../tests/api-mocking/mock-responses/defi-api-mocks'; +import { setupMockRequest } from '../../../api-mocking/helpers/mockHelpers'; +import { defiPositionsWithData } from '../../../api-mocking/mock-responses/defi-api-mocks'; describe(SmokeNetworkAbstractions('View DeFi details'), () => { it('open the Aave V3 position details', async () => { diff --git a/e2e/specs/multisrp/add-account.spec.ts b/tests/smoke/multisrp/add-account.spec.ts similarity index 70% rename from e2e/specs/multisrp/add-account.spec.ts rename to tests/smoke/multisrp/add-account.spec.ts index b2ec34300c3..cd86e0fe5fd 100644 --- a/e2e/specs/multisrp/add-account.spec.ts +++ b/tests/smoke/multisrp/add-account.spec.ts @@ -1,15 +1,15 @@ -import { SmokeWalletPlatform } from '../../tags'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import WalletView from '../../pages/wallet/WalletView'; -import { loginToApp } from '../../viewHelper'; -import Assertions from '../../../tests/framework/Assertions'; -import AccountListBottomSheet from '../../pages/wallet/AccountListBottomSheet'; -import AddAccountBottomSheet from '../../pages/wallet/AddAccountBottomSheet'; -import SRPListItemComponent from '../../pages/wallet/MultiSrp/Common/SRPListItemComponent'; -import AddNewHdAccountComponent from '../../pages/wallet/MultiSrp/AddAccountToSrp/AddNewHdAccountComponent'; -import { setupRemoteFeatureFlagsMock } from '../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { remoteFeatureMultichainAccountsAccountDetailsV2 } from '../../../tests/api-mocking/mock-responses/feature-flags-mocks'; +import { SmokeWalletPlatform } from '../../../e2e/tags'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper.ts'; +import WalletView from '../../../e2e/pages/wallet/WalletView.ts'; +import { loginToApp } from '../../../e2e/viewHelper.ts'; +import Assertions from '../../framework/Assertions.ts'; +import AccountListBottomSheet from '../../../e2e/pages/wallet/AccountListBottomSheet.ts'; +import AddAccountBottomSheet from '../../../e2e/pages/wallet/AddAccountBottomSheet.ts'; +import SRPListItemComponent from '../../../e2e/pages/wallet/MultiSrp/Common/SRPListItemComponent.ts'; +import AddNewHdAccountComponent from '../../../e2e/pages/wallet/MultiSrp/AddAccountToSrp/AddNewHdAccountComponent.ts'; +import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper.ts'; +import { remoteFeatureMultichainAccountsAccountDetailsV2 } from '../../api-mocking/mock-responses/feature-flags-mocks.ts'; const SRP_1 = { index: 1, diff --git a/e2e/specs/multisrp/export-srp-from-account-actions.spec.ts b/tests/smoke/multisrp/export-srp-from-account-actions.spec.ts similarity index 76% rename from e2e/specs/multisrp/export-srp-from-account-actions.spec.ts rename to tests/smoke/multisrp/export-srp-from-account-actions.spec.ts index 1749b61d795..53a5067f801 100644 --- a/e2e/specs/multisrp/export-srp-from-account-actions.spec.ts +++ b/tests/smoke/multisrp/export-srp-from-account-actions.spec.ts @@ -1,11 +1,14 @@ -import { SmokeWalletPlatform } from '../../tags'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { loginToApp } from '../../viewHelper'; -import { goToAccountActions, completeSrpQuiz } from './utils'; -import { defaultOptions } from '../../../tests/seeder/anvil-manager'; -import { setupRemoteFeatureFlagsMock } from '../../../tests/api-mocking/helpers/remoteFeatureFlagsHelper'; -import { remoteFeatureMultichainAccountsAccountDetailsV2 } from '../../../tests/api-mocking/mock-responses/feature-flags-mocks'; +import { SmokeWalletPlatform } from '../../../e2e/tags'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper.ts'; +import { loginToApp } from '../../../e2e/viewHelper.ts'; +import { + goToAccountActions, + completeSrpQuiz, +} from '../../flows/accounts.flow.ts'; +import { defaultOptions } from '../../seeder/anvil-manager.ts'; +import { setupRemoteFeatureFlagsMock } from '../../api-mocking/helpers/remoteFeatureFlagsHelper.ts'; +import { remoteFeatureMultichainAccountsAccountDetailsV2 } from '../../api-mocking/mock-responses/feature-flags-mocks.ts'; const FIRST_DEFAULT_HD_KEYRING_ACCOUNT = 0; const FIRST_IMPORTED_HD_KEYRING_ACCOUNT = 2; diff --git a/e2e/specs/multisrp/export-srp-from-settings.spec.ts b/tests/smoke/multisrp/export-srp-from-settings.spec.ts similarity index 76% rename from e2e/specs/multisrp/export-srp-from-settings.spec.ts rename to tests/smoke/multisrp/export-srp-from-settings.spec.ts index 0428388dea0..0aa0973c11d 100644 --- a/e2e/specs/multisrp/export-srp-from-settings.spec.ts +++ b/tests/smoke/multisrp/export-srp-from-settings.spec.ts @@ -1,9 +1,12 @@ -import { SmokeWalletPlatform } from '../../tags'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { loginToApp } from '../../viewHelper'; -import { startExportForKeyring, completeSrpQuiz } from './utils'; -import { defaultOptions } from '../../../tests/seeder/anvil-manager'; +import { SmokeWalletPlatform } from '../../../e2e/tags'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper.ts'; +import { loginToApp } from '../../../e2e/viewHelper.ts'; +import { + startExportForKeyring, + completeSrpQuiz, +} from '../../flows/accounts.flow.ts'; +import { defaultOptions } from '../../seeder/anvil-manager.ts'; const SRP_1 = { index: 1, diff --git a/e2e/specs/ramps/offramp-token-amount.spec.ts b/tests/smoke/ramps/offramp-token-amount.spec.ts similarity index 65% rename from e2e/specs/ramps/offramp-token-amount.spec.ts rename to tests/smoke/ramps/offramp-token-amount.spec.ts index 707f2330313..f632d28124c 100644 --- a/e2e/specs/ramps/offramp-token-amount.spec.ts +++ b/tests/smoke/ramps/offramp-token-amount.spec.ts @@ -1,18 +1,15 @@ -import { loginToApp } from '../../viewHelper'; -import WalletView from '../../pages/wallet/WalletView'; -import FundActionMenu from '../../pages/UI/FundActionMenu'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { SmokeRamps } from '../../tags'; -import { CustomNetworks } from '../../../tests/resources/networks.e2e'; -import BuildQuoteView from '../../pages/Ramps/BuildQuoteView'; -import Assertions from '../../../tests/framework/Assertions'; -import { - RampsRegions, - RampsRegionsEnum, -} from '../../../tests/framework/Constants'; +import { loginToApp } from '../../../e2e/viewHelper.ts'; +import WalletView from '../../../e2e/pages/wallet/WalletView.ts'; +import FundActionMenu from '../../../e2e/pages/UI/FundActionMenu.ts'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper.ts'; +import { SmokeRamps } from '../../../e2e/tags'; +import { CustomNetworks } from '../../resources/networks.e2e'; +import BuildQuoteView from '../../../e2e/pages/Ramps/BuildQuoteView.ts'; +import Assertions from '../../framework/Assertions.ts'; +import { RampsRegions, RampsRegionsEnum } from '../../framework/Constants.ts'; import { Mockttp } from 'mockttp'; -import { setupRegionAwareOnRampMocks } from '../../../tests/api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup'; +import { setupRegionAwareOnRampMocks } from '../../api-mocking/mock-responses/ramps/ramps-region-aware-mock-setup.ts'; describe(SmokeRamps('Off-ramp token amounts'), () => { beforeEach(async () => { diff --git a/e2e/specs/stake/stake-action-smoke.spec.ts b/tests/smoke/stake/stake-action-smoke.spec.ts similarity index 74% rename from e2e/specs/stake/stake-action-smoke.spec.ts rename to tests/smoke/stake/stake-action-smoke.spec.ts index ca82eac8570..3612d27edab 100644 --- a/e2e/specs/stake/stake-action-smoke.spec.ts +++ b/tests/smoke/stake/stake-action-smoke.spec.ts @@ -1,17 +1,17 @@ -import { withFixtures } from '../../../tests/framework/fixtures/FixtureHelper'; -import { LocalNode, LocalNodeType } from '../../../tests/framework/types'; -import { loginToApp } from '../../viewHelper'; -import TabBarComponent from '../../pages/wallet/TabBarComponent'; -import ActivitiesView from '../../pages/Transactions/ActivitiesView'; -import { ActivitiesViewSelectorsText } from '../../../app/components/Views/ActivityView/ActivitiesView.testIds'; -import FixtureBuilder from '../../../tests/framework/fixtures/FixtureBuilder'; -import WalletView from '../../pages/wallet/WalletView'; -import NetworkListModal from '../../pages/Network/NetworkListModal'; -import { SmokeTrade } from '../../tags'; -import Assertions from '../../../tests/framework/Assertions'; -import StakeView from '../../pages/Stake/StakeView'; -import { AnvilPort } from '../../../tests/framework/fixtures/FixtureUtils'; -import { AnvilManager } from '../../../tests/seeder/anvil-manager'; +import { withFixtures } from '../../framework/fixtures/FixtureHelper.ts'; +import { LocalNode, LocalNodeType } from '../../framework/types.ts'; +import { loginToApp } from '../../../e2e/viewHelper.ts'; +import TabBarComponent from '../../../e2e/pages/wallet/TabBarComponent.ts'; +import ActivitiesView from '../../../e2e/pages/Transactions/ActivitiesView.ts'; +import { ActivitiesViewSelectorsText } from '../../../app/components/Views/ActivityView/ActivitiesView.testIds.ts'; +import FixtureBuilder from '../../framework/fixtures/FixtureBuilder.ts'; +import WalletView from '../../../e2e/pages/wallet/WalletView.ts'; +import NetworkListModal from '../../../e2e/pages/Network/NetworkListModal.ts'; +import { SmokeTrade } from '../../../e2e/tags'; +import Assertions from '../../framework/Assertions.ts'; +import StakeView from '../../../e2e/pages/Stake/StakeView.ts'; +import { AnvilPort } from '../../framework/fixtures/FixtureUtils.ts'; +import { AnvilManager } from '../../seeder/anvil-manager.ts'; describe(SmokeTrade('Stake from Actions'), (): void => { const FIRST_ROW: number = 0; diff --git a/tests/smoke/wallet/import-srp.spec.ts b/tests/smoke/wallet/import-srp.spec.ts index a58cd6ad6fc..002371344af 100644 --- a/tests/smoke/wallet/import-srp.spec.ts +++ b/tests/smoke/wallet/import-srp.spec.ts @@ -5,7 +5,7 @@ import WalletView from '../../../e2e/pages/wallet/WalletView'; import { loginToApp } from '../../../e2e/viewHelper'; import Assertions from '../../framework/Assertions'; import ImportSrpView from '../../../e2e/pages/importSrp/ImportSrpView'; -import { goToImportSrp, inputSrp } from '../../../e2e/specs/multisrp/utils'; +import { goToImportSrp, inputSrp } from '../../flows/accounts.flow.ts'; import { IDENTITY_TEAM_SEED_PHRASE } from '../../../e2e/specs/identity/utils/constants'; // We now have account indexes "per wallets", thus the new account for that new SRP (wallet), will diff --git a/yarn.lock b/yarn.lock index 98a08efec24..5925bef3176 100644 --- a/yarn.lock +++ b/yarn.lock @@ -34996,6 +34996,7 @@ __metadata: jest-junit: "npm:^15.0.0" jetifier: "npm:2.0.0" js-sha3: "npm:0.9.3" + js-yaml: "npm:^4.1.0" koa: "npm:^2.14.2" lint-staged: "npm:10.5.4" listr2: "npm:^8.0.2"