diff --git a/.github/workflows/auto-rc-ota-build-core.yml b/.github/workflows/auto-rc-ota-build-core.yml index 2f7e63d830c8..f6ab2a9c92d2 100644 --- a/.github/workflows/auto-rc-ota-build-core.yml +++ b/.github/workflows/auto-rc-ota-build-core.yml @@ -2,12 +2,14 @@ # # Auto RC OTA / build core (reusable) # -# Shared logic for the Auto RC flow (build-rc-auto.yml): detect an OTA_VERSION bump and either -# dispatch push-eas-update.yml, or fall through to build.yml. +# Shared logic for the Auto RC flow (build-rc-auto.yml): detect whether the push is an +# OTA_VERSION bump and, if so, skip the native build (OTA-only changes are published +# separately). Otherwise fall through to a native build.yml build (and TestFlight for iOS). # -# Runway's manual entry workflows no longer use this file — they call the dedicated OTA-only or -# build-only workflows (runway-ota-*.yml, runway-*-builds.yml) directly. Kept here to preserve -# automatic OTA-vs-build detection on every push to a release branch. +# This workflow does not push OTA updates — OTA publishing is handled outside this flow. +# +# Runway's manual entry workflows do not use this file — they call the dedicated OTA-only or +# build-only workflows (runway-ota-*.yml, runway-*-builds.yml) directly. # ############################################################################################## name: Auto RC OTA Build Core @@ -16,7 +18,7 @@ on: workflow_call: inputs: platform: - description: 'Target platform passed to push-eas-update and build.yml (android or ios)' + description: 'Target platform passed to build.yml (android or ios)' required: true type: string source_branch: @@ -26,21 +28,11 @@ on: required: false type: string default: '' - ota_channel: - description: 'push-eas-update channel input (e.g. rc, production)' - required: false - type: string - default: rc build_name: description: 'build.yml build_name (e.g. main-rc, main-prod)' required: false type: string default: main-rc - create_production_ota_tag: - description: 'If true, create OTA release tag after production trigger-ota (callers: *production* only)' - required: false - type: boolean - default: false environment: description: 'Build environment / track passed to upload-to-testflight (e.g. rc, prod)' required: false @@ -48,19 +40,18 @@ on: default: 'rc' outputs: semantic_version: - description: 'package.json version at the built commit (empty when OTA path taken)' + description: 'package.json version at the built commit (empty when OTA bump skips the build)' value: ${{ jobs.trigger-build.outputs.semantic_version }} ios_version_code: - description: 'iOS CURRENT_PROJECT_VERSION at the built commit (empty when OTA path taken)' + description: 'iOS CURRENT_PROJECT_VERSION at the built commit (empty when OTA bump skips the build)' value: ${{ jobs.trigger-build.outputs.ios_version_code }} android_version_code: - description: 'Android versionCode at the built commit (empty when OTA path taken)' + description: 'Android versionCode at the built commit (empty when OTA bump skips the build)' value: ${{ jobs.trigger-build.outputs.android_version_code }} permissions: - contents: write - pull-requests: read - actions: write + contents: read + pull-requests: read # required by runway-ota-resolve-context.yml id-token: write # required by build.yml jobs: @@ -71,34 +62,6 @@ jobs: source_branch: ${{ inputs.source_branch }} secrets: inherit - validate-ota-pr: - name: Validate PR for OTA - needs: resolve-context - if: needs.resolve-context.outputs.ota_bump == 'true' - runs-on: ubuntu-latest - steps: - - name: Validate PR number - run: | - if [[ -z "${{ needs.resolve-context.outputs.pr_number }}" ]]; then - echo "::error::No PR found for this branch. OTA update requires a PR number." - echo "::error::If you ran the workflow manually (workflow_dispatch), select your release branch in the 'Use workflow from' dropdown (e.g. release/7.71.0), not main." - exit 1 - fi - echo "Using PR #${{ needs.resolve-context.outputs.pr_number }}" - - trigger-ota: - name: Trigger OTA update - needs: [resolve-context, validate-ota-pr] - if: needs.resolve-context.outputs.ota_bump == 'true' - uses: ./.github/workflows/push-eas-update.yml - with: - pr_number: ${{ needs.resolve-context.outputs.pr_number }} - base_branch: ${{ needs.resolve-context.outputs.base_ref }} - message: ${{ needs.resolve-context.outputs.ota_version }} - channel: ${{ inputs.ota_channel }} - platform: ${{ inputs.platform }} - secrets: inherit - trigger-build: name: Trigger build mobile app needs: resolve-context @@ -111,16 +74,6 @@ jobs: upload_to_sentry: true secrets: inherit - create-ota-production-tag: - name: Create OTA production release tag - needs: [resolve-context, trigger-ota] - if: ${{ inputs.create_production_ota_tag == true }} - uses: ./.github/workflows/runway-create-ota-production-tag.yml - with: - tag_name: ${{ needs.resolve-context.outputs.ota_version }} - checkout_ref: ${{ inputs.source_branch || github.ref_name }} - secrets: inherit - upload-ios-testflight: name: Upload iOS to TestFlight needs: [trigger-build] diff --git a/.github/workflows/runway-create-ota-production-tag.yml b/.github/workflows/runway-create-ota-production-tag.yml deleted file mode 100644 index 6119c040b7af..000000000000 --- a/.github/workflows/runway-create-ota-production-tag.yml +++ /dev/null @@ -1,69 +0,0 @@ -############################################################################################## -# -# Reusable: create SemVer release tag after production OTA (idempotent). -# -# Callers: runway_*_production_workflow.yml after trigger-ota succeeds. -# Skips if the tag already points at the checked-out commit; fails if the tag exists elsewhere. -# -############################################################################################## -name: Create OTA production release tag - -on: - workflow_call: - inputs: - tag_name: - description: 'Annotated tag to create; must match OTA_VERSION (app/constants/ota.ts) / decide ota_version' - required: true - type: string - checkout_ref: - description: 'Branch or ref that received the OTA (same as workflow source)' - required: true - type: string - -permissions: - contents: write - -jobs: - create-tag: - name: Create release tag (production OTA) - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ inputs.checkout_ref }} - - - name: Create or skip release tag - env: - TAG_NAME: ${{ inputs.tag_name }} - run: | - set -euo pipefail - if [[ -z "${TAG_NAME}" ]]; then - echo '::error::tag_name is empty; cannot create release tag' - exit 1 - fi - if [[ ! "${TAG_NAME}" =~ ^v[^[:space:]]+$ ]]; then - echo "::error::tag_name must be non-empty and start with v (no spaces), got: ${TAG_NAME}" - exit 1 - fi - - HEAD_SHA=$(git rev-parse HEAD) - git fetch origin --tags --force 2>/dev/null || true - - if git rev-parse -q --verify "refs/tags/${TAG_NAME}" >/dev/null 2>&1; then - TAG_SHA=$(git rev-parse "${TAG_NAME}^{commit}") - if [[ "${HEAD_SHA}" == "${TAG_SHA}" ]]; then - echo "Tag \`${TAG_NAME}\` already points at this commit (${HEAD_SHA}); skipping create and push." - exit 0 - fi - echo "::error::Tag \`${TAG_NAME}\` already exists at ${TAG_SHA} but HEAD is ${HEAD_SHA}. Refusing to move the tag." - exit 1 - fi - - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - - git tag -a "${TAG_NAME}" -m "Production OTA release ${TAG_NAME}" - git push origin "refs/tags/${TAG_NAME}" - echo "Created and pushed tag \`${TAG_NAME}\` at ${HEAD_SHA}" diff --git a/.github/workflows/runway-ota-production.yml b/.github/workflows/runway-ota-production.yml index 1fa49b2aa59a..55a9ae703a80 100644 --- a/.github/workflows/runway-ota-production.yml +++ b/.github/workflows/runway-ota-production.yml @@ -2,8 +2,7 @@ # # Runway OTA Production # -# Triggered from Runway to push an OTA update to the production channel (iOS + Android) and -# create the corresponding `v` release tag. +# Triggered from Runway to push an OTA update to the production channel (iOS + Android). # # This workflow does not build binaries and does not bump the build version — the release PR is # expected to bump OTA_VERSION (app/constants/ota.ts) before dispatch. @@ -16,13 +15,13 @@ on: inputs: source_branch: description: >- - Optional branch, tag, or SHA for OTA publish + tag creation. + Optional branch, tag, or SHA for OTA publish. Empty uses the branch selected in the "Use workflow from" UI. required: false type: string permissions: - contents: write # required to push the v tag + contents: read pull-requests: read id-token: write # required by push-eas-update.yml @@ -59,12 +58,3 @@ jobs: channel: production platform: all secrets: inherit - - create-ota-production-tag: - name: Create OTA production release tag - needs: [resolve-context, push-ota] - uses: ./.github/workflows/runway-create-ota-production-tag.yml - with: - tag_name: ${{ needs.resolve-context.outputs.ota_version }} - checkout_ref: ${{ inputs.source_branch || github.ref_name }} - secrets: inherit diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d464c874f74..e8048e91fa5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Uncategorized +## [7.78.1] + +### Fixed -- chore(deps): bump the `@metamask/tron-wallet-snap` to `^1.25.6` (#30200) +- Fixed a crash caused by CloseEvent dispatch on WebSocket failing instanceof validation (#30612) ## [7.78.0] @@ -11561,7 +11563,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.78.0...HEAD +[Unreleased]: https://github.com/MetaMask/metamask-mobile/compare/v7.78.1...HEAD +[7.78.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.78.0...v7.78.1 [7.78.0]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.2...v7.78.0 [7.77.2]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.1...v7.77.2 [7.77.1]: https://github.com/MetaMask/metamask-mobile/compare/v7.77.0...v7.77.1 diff --git a/app/components/UI/Bridge/components/BatchSellDestinationTokenSelectorModal/index.tsx b/app/components/UI/Bridge/components/BatchSellDestinationTokenSelectorModal/index.tsx index 1c06e9390e99..bd03756a8c1c 100644 --- a/app/components/UI/Bridge/components/BatchSellDestinationTokenSelectorModal/index.tsx +++ b/app/components/UI/Bridge/components/BatchSellDestinationTokenSelectorModal/index.tsx @@ -36,6 +36,7 @@ import { RootState } from '../../../../../reducers'; import { BridgeToken } from '../../types'; import { formatTokenBalance } from '../../utils'; import { BatchSellDestinationTokenSelectorModalSelectorsIDs } from './BatchSellDestinationTokenSelectorModal.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; const getTokenKey = (token: BridgeToken) => `${token.chainId}:${token.address}`; @@ -86,6 +87,7 @@ export function BatchSellDestinationTokenSelectorModal() { chainIds: destinationChainIds ?? (sourceChainId ? [sourceChainId] : undefined), }); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -104,6 +106,7 @@ export function BatchSellDestinationTokenSelectorModal() { ref={sheetRef} goBack={navigation.goBack} testID={BatchSellDestinationTokenSelectorModalSelectorsIDs.SHEET} + twClassName={surfaceClass} > @@ -362,6 +364,7 @@ export function BatchSellFinalReviewModal() { navigation.replace(sourceModal.screen, sourceModal.params) : undefined; + const surfaceClass = useElevatedSurface(); return ( navigation.replace(sourceModal.screen, sourceModal.params) : undefined; + const surfaceClass = useElevatedSurface(); return ( >>(); const sourceTokens = useSelector(selectBatchSellSourceTokens); + const surfaceClass = useElevatedSurface(); const batchSellQuoteData = useBatchSellQuoteData({ shouldUpdateBatchSellTrades: false, }); @@ -48,6 +50,7 @@ export function BatchSellQuoteDetailsModal() { { sheetRef.current?.onCloseBottomSheet(); @@ -49,6 +51,7 @@ export function HighRateAlertModal() { ref={sheetRef} goBack={goBack} testID={HighRateAlertModalSelectorsIDs.SHEET} + twClassName={surfaceClass} > { const sheetRef = useRef(null); const [loading, setLoading] = useState(false); const { location } = useParams(); - + const surfaceClass = useElevatedSurface(); const sourceToken = useSelector(selectSourceToken); const tokenBalance = useLatestBalance({ address: sourceToken?.address, @@ -62,7 +63,11 @@ export const MissingPriceModal = () => { }, [confirmBridge]); return ( - + { const { goBack } = useNavigation(); @@ -42,6 +43,7 @@ export const PriceImpactModal = () => { const priceImpactViewData = usePriceImpactViewData( activeQuote?.quote.priceData?.priceImpact, ); + const surfaceClass = useElevatedSurface(); const isDangerousPriceImpact = useMemo( () => @@ -62,7 +64,7 @@ export const PriceImpactModal = () => { }, [confirmBridge]); return ( - + = ({ isVisible, onClose, onContactSupport, }) => { + const surfaceClass = useElevatedSurface(); + if (!isVisible) return null; return ( { const bottomSheetRef = useRef(null); const { enableBackupAndSync, trackEnableBackupAndSyncEvent } = useParams(); const dispatch = useThunkDispatch(); + const surfaceClass = useElevatedSurface(); const enableBasicFunctionality = async () => { await dispatch(toggleBasicFunctionality(true)); @@ -51,7 +54,7 @@ const ConfirmTurnOnBackupAndSyncModal = () => { }; return ( - + { const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); const { apy } = useParams(); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -39,6 +41,7 @@ const MoneyApyInfoSheet = () => { goBack={handleGoBack} testID={MoneyApyInfoSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > diff --git a/app/components/UI/Money/components/MoneyBalanceInfoSheet/MoneyBalanceInfoSheet.tsx b/app/components/UI/Money/components/MoneyBalanceInfoSheet/MoneyBalanceInfoSheet.tsx index 3b3ec1a9841e..eaf32842a2eb 100644 --- a/app/components/UI/Money/components/MoneyBalanceInfoSheet/MoneyBalanceInfoSheet.tsx +++ b/app/components/UI/Money/components/MoneyBalanceInfoSheet/MoneyBalanceInfoSheet.tsx @@ -4,19 +4,21 @@ import { useNavigation } from '@react-navigation/native'; import { BottomSheet, BottomSheetHeader, - type BottomSheetRef, Text, TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { strings } from '../../../../../../locales/i18n'; import { useStyles } from '../../../../../component-library/hooks'; import styleSheet from './MoneyBalanceInfoSheet.styles'; import { MoneyBalanceInfoSheetTestIds } from './MoneyBalanceInfoSheet.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; const MoneyBalanceInfoSheet = () => { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -32,6 +34,7 @@ const MoneyBalanceInfoSheet = () => { goBack={handleGoBack} testID={MoneyBalanceInfoSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > { @@ -22,6 +24,7 @@ const MoneyEarnCryptoInfoSheet = () => { const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); const { apyPercent } = useMoneyAccountBalance(); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -37,6 +40,7 @@ const MoneyEarnCryptoInfoSheet = () => { goBack={handleGoBack} testID={MoneyEarnCryptoInfoSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -32,6 +34,7 @@ const MoneyEarningsInfoSheet = () => { goBack={handleGoBack} testID={MoneyEarningsInfoSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > diff --git a/app/components/UI/Money/components/MoneyLinkCardSheet/MoneyLinkCardSheet.tsx b/app/components/UI/Money/components/MoneyLinkCardSheet/MoneyLinkCardSheet.tsx index 76b676ed8105..907791982562 100644 --- a/app/components/UI/Money/components/MoneyLinkCardSheet/MoneyLinkCardSheet.tsx +++ b/app/components/UI/Money/components/MoneyLinkCardSheet/MoneyLinkCardSheet.tsx @@ -26,6 +26,7 @@ import mmCardRegular from '../../../../../images/mm_card_regular.png'; import mmCardMetal from '../../../../../images/mm_card_metal.png'; import styleSheet from './MoneyLinkCardSheet.styles'; import { MoneyLinkCardSheetTestIds } from './MoneyLinkCardSheet.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; /** * "Spend and earn" confirmation bottom sheet shown before the Money Account ↔ @@ -43,6 +44,7 @@ const MoneyLinkCardSheet = () => { const { confirmLinkInBackground } = useMoneyAccountCardLinkage(); const { apyPercent } = useMoneyAccountBalance(); const cardHomeData = useSelector(selectCardHomeData); + const surfaceClass = useElevatedSurface(); const isMetalCard = cardHomeData?.card?.type === CardType.METAL; const handleGoBack = useCallback(() => { @@ -83,6 +85,7 @@ const MoneyLinkCardSheet = () => { goBack={handleGoBack} testID={MoneyLinkCardSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const closeAndNavigate = useCallback((navigateFn: () => void) => { sheetRef.current?.onCloseBottomSheet(navigateFn); @@ -86,6 +88,7 @@ const MoneyMoreSheet = () => { goBack={handleGoBack} testID={MoneyMoreSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > sheetRef.current?.onCloseBottomSheet()}> diff --git a/app/components/UI/Money/components/MoneyTransactionDetailsSheet/MoneyTransactionDetailsSheet.tsx b/app/components/UI/Money/components/MoneyTransactionDetailsSheet/MoneyTransactionDetailsSheet.tsx index 09d66cb94f95..11fcd7cda8a7 100644 --- a/app/components/UI/Money/components/MoneyTransactionDetailsSheet/MoneyTransactionDetailsSheet.tsx +++ b/app/components/UI/Money/components/MoneyTransactionDetailsSheet/MoneyTransactionDetailsSheet.tsx @@ -2,10 +2,10 @@ import React, { useCallback, useRef } from 'react'; import { useNavigation } from '@react-navigation/native'; import { BottomSheet, - type BottomSheetRef, BottomSheetHeader, Text, TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { type TransactionMeta, @@ -14,6 +14,7 @@ import { import { strings } from '../../../../../../locales/i18n'; import { TransactionDetails } from '../../../../Views/confirmations/components/activity/transaction-details/transaction-details'; import { useTransactionDetails } from '../../../../Views/confirmations/hooks/activity/useTransactionDetails'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; import { MoneyReceivedDetails } from './MoneyReceivedDetails'; const RECEIVED_TYPES: TransactionType[] = [ @@ -58,6 +59,7 @@ const MoneyTransactionDetailsSheet = () => { const sheetRef = useRef(null); const navigation = useNavigation(); const { transactionMeta } = useTransactionDetails(); + const surfaceClass = useElevatedSurface(); const title = getTitle(transactionMeta); const isReceived = Boolean( transactionMeta?.type && RECEIVED_TYPES.includes(transactionMeta.type), @@ -73,6 +75,7 @@ const MoneyTransactionDetailsSheet = () => { isFullscreen goBack={navigation.goBack} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > {title} diff --git a/app/components/UI/Money/components/MoneyTransferSheet/MoneyTransferSheet.tsx b/app/components/UI/Money/components/MoneyTransferSheet/MoneyTransferSheet.tsx index b37f8850e10e..06c6eb81f1d5 100644 --- a/app/components/UI/Money/components/MoneyTransferSheet/MoneyTransferSheet.tsx +++ b/app/components/UI/Money/components/MoneyTransferSheet/MoneyTransferSheet.tsx @@ -4,15 +4,15 @@ import { useNavigation } from '@react-navigation/native'; import { BottomSheet, BottomSheetHeader, - type BottomSheetRef, FontWeight, Icon, + IconColor, IconName, IconSize, - IconColor, Text, TextColor, TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import Tag from '../../../../../component-library/components/Tags/Tag'; import { strings } from '../../../../../../locales/i18n'; @@ -21,6 +21,7 @@ import { useMoneyAccountWithdrawal } from '../../hooks/useMoneyAccount'; import Logger from '../../../../../util/Logger'; import styleSheet from './MoneyTransferSheet.styles'; import { MoneyTransferSheetTestIds } from './MoneyTransferSheet.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; interface ActiveOption { label: string; @@ -40,6 +41,7 @@ const MoneyTransferSheet = () => { const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); const { initiateWithdrawal } = useMoneyAccountWithdrawal(); + const surfaceClass = useElevatedSurface(); const handleGoBack = useCallback(() => { navigation.goBack(); @@ -106,6 +108,7 @@ const MoneyTransferSheet = () => { goBack={handleGoBack} testID={MoneyTransferSheetTestIds.CONTAINER} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > sheetRef.current?.onCloseBottomSheet()}> diff --git a/app/components/UI/NftGrid/NftGridItemBottomSheet.tsx b/app/components/UI/NftGrid/NftGridItemBottomSheet.tsx index 40c2819218ae..d295e323c528 100644 --- a/app/components/UI/NftGrid/NftGridItemBottomSheet.tsx +++ b/app/components/UI/NftGrid/NftGridItemBottomSheet.tsx @@ -1,12 +1,12 @@ import { - Box, + BottomSheet, BottomSheetHeader, + BottomSheetRef, + Box, Button, ButtonVariant, Text, TextVariant, - BottomSheet, - BottomSheetRef, } from '@metamask/design-system-react-native'; import React, { useCallback, useRef } from 'react'; import { Alert, Modal, View } from 'react-native'; @@ -14,6 +14,7 @@ import { strings } from '../../../../locales/i18n'; import { Nft } from '@metamask/assets-controllers'; import Engine from '../../../core/Engine'; import { toHex } from '@metamask/controller-utils'; +import { useElevatedSurface } from '../../../util/theme/themeUtils'; interface NftGridItemBottomSheetProps { isVisible: boolean; @@ -27,6 +28,7 @@ const NftGridItemBottomSheet: React.FC = ({ nft, }) => { const sheetRef = useRef(null); + const surfaceClass = useElevatedSurface(); const handleSheetClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -75,7 +77,11 @@ const NftGridItemBottomSheet: React.FC = ({ return ( - + {strings('wallet.collectible_action_title')} diff --git a/app/components/UI/Perps/Views/PerpsTooltipView/PerpsTooltipView.tsx b/app/components/UI/Perps/Views/PerpsTooltipView/PerpsTooltipView.tsx index 57ec0878b29b..07d56183135c 100644 --- a/app/components/UI/Perps/Views/PerpsTooltipView/PerpsTooltipView.tsx +++ b/app/components/UI/Perps/Views/PerpsTooltipView/PerpsTooltipView.tsx @@ -15,6 +15,7 @@ import { strings } from '../../../../../../locales/i18n'; import { PerpsTooltipContentKey } from '../../components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.types'; import { tooltipContentRegistry } from '../../components/PerpsBottomSheetTooltip/content/contentRegistry'; import { PerpsBottomSheetTooltipSelectorsIDs } from '../../Perps.testIds'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; interface PerpsTooltipViewRouteParams { contentKey: PerpsTooltipContentKey; @@ -26,6 +27,7 @@ const PerpsTooltipView: React.FC = () => { const route = useRoute, string>>(); const bottomSheetRef = useRef(null); + const surfaceClass = useElevatedSurface(); const { contentKey, data } = route.params || {}; @@ -67,6 +69,7 @@ const PerpsTooltipView: React.FC = () => { ref={bottomSheetRef} goBack={navigation.goBack} testID="perps-tooltip-bottom-sheet" + twClassName={surfaceClass} > {!hasCustomHeader && ( diff --git a/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx b/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx index 820596ae8be8..1891022a10b3 100644 --- a/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx +++ b/app/components/UI/Perps/components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.tsx @@ -22,6 +22,7 @@ import { } from '@metamask/perps-controller'; import { usePerpsEventTracking } from '../../hooks/usePerpsEventTracking'; import { MetaMetricsEvents } from '../../../../../core/Analytics/MetaMetrics.events'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; /** * Tip: If want to render the PerpsBottomSheetTooltip from the root (not constrained by a parent component), @@ -81,6 +82,7 @@ const PerpsBottomSheetTooltip = React.memo( }; const { track } = usePerpsEventTracking(); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { bottomSheetRef.current?.onCloseBottomSheet(); @@ -122,7 +124,12 @@ const PerpsBottomSheetTooltip = React.memo( if (!isVisible || !title) return null; return ( - + {!hasCustomHeader && ( { const navigation = useNavigation(); const { styles } = useStyles(createStyles, {}); + const surfaceClass = useElevatedSurface(); const refreshRate = DEPOSIT_CONFIG.RefreshRate / 1000; // Convert to seconds const handleGetNewQuote = () => { @@ -27,7 +29,7 @@ const PerpsQuoteExpiredModal = () => { }; return ( - + {strings('perps.deposit.quote_expired_modal.title')} diff --git a/app/components/UI/Predict/components/PredictPreviewSheet/PredictPreviewSheet.tsx b/app/components/UI/Predict/components/PredictPreviewSheet/PredictPreviewSheet.tsx index 595d9540b31d..eff77b3d7774 100644 --- a/app/components/UI/Predict/components/PredictPreviewSheet/PredictPreviewSheet.tsx +++ b/app/components/UI/Predict/components/PredictPreviewSheet/PredictPreviewSheet.tsx @@ -16,6 +16,7 @@ import { usePredictBottomSheet, type PredictBottomSheetRef, } from '../../hooks/usePredictBottomSheet'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; interface PredictPreviewSheetProps { renderHeader?: () => React.ReactNode; @@ -55,6 +56,7 @@ const PredictPreviewSheet = forwardRef< handleSheetClosed, getRefHandlers, } = usePredictBottomSheet({ onDismiss }); + const surfaceClass = useElevatedSurface(); useImperativeHandle(ref, getRefHandlers, [getRefHandlers]); @@ -69,6 +71,7 @@ const PredictPreviewSheet = forwardRef< isFullscreen={isFullscreen} onClose={handleSheetClosed} testID={testID} + twClassName={surfaceClass} > ( handleSheetClosed, getRefHandlers, } = usePredictBottomSheet(); + const surfaceClass = useElevatedSurface(); useImperativeHandle(ref, getRefHandlers, [getRefHandlers]); @@ -44,6 +46,7 @@ const PredictWithdrawUnavailableSheet = forwardRef( isInteractable onClose={handleSheetClosed} testID={PREDICT_BALANCE_TEST_IDS.WITHDRAW_UNAVAILABLE_SHEET} + twClassName={surfaceClass} > { }); }); + describe('selectPredictHomeRedesignEnabledFlag', () => { + it('returns false when flag is missing', () => { + const result = selectPredictHomeRedesignEnabledFlag( + mockedEmptyFlagsState, + ); + + expect(result).toBe(false); + }); + + it('returns true when flag is enabled and version requirement is met', () => { + mockHasMinimumRequiredVersion.mockReturnValue(true); + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + minimumVersion: '1.0.0', + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(true); + }); + + it('returns false when flag is disabled', () => { + mockHasMinimumRequiredVersion.mockReturnValue(true); + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: false, + minimumVersion: '1.0.0', + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(false); + }); + + it('returns false when flag is malformed', () => { + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: 'true', + minimumVersion: '1.0.0', + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(false); + }); + + it('returns false when app version is below minimum required', () => { + mockHasMinimumRequiredVersion.mockReturnValue(false); + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + minimumVersion: '99.0.0', + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(false); + }); + + it('returns false when minimumVersion is the default empty string', () => { + const state = { + engine: { + backgroundState: { + RemoteFeatureFlagController: { + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + }, + }, + cacheTimestamp: 0, + }, + }, + }, + }; + + const result = selectPredictHomeRedesignEnabledFlag(state); + + expect(result).toBe(false); + }); + }); + describe('selectPredictHomepageDiscoveryNbaChampionEnabledFlag', () => { it('returns false when the remote flag is disabled', () => { mockHasMinimumRequiredVersion.mockReturnValue(true); diff --git a/app/components/UI/Predict/selectors/featureFlags/index.ts b/app/components/UI/Predict/selectors/featureFlags/index.ts index 7cd3ccedd50c..084d315a76b1 100644 --- a/app/components/UI/Predict/selectors/featureFlags/index.ts +++ b/app/components/UI/Predict/selectors/featureFlags/index.ts @@ -183,6 +183,11 @@ export const selectPredictPortfolioEnabledFlag = createSelector( (flags) => flags.predictPortfolioEnabled, ); +export const selectPredictHomeRedesignEnabledFlag = createSelector( + selectPredictFeatureFlags, + (flags) => flags.predictHomeRedesignEnabled, +); + export const selectPredictFeaturedCarouselEnabledFlag = createSelector( selectRemoteFeatureFlags, (remoteFeatureFlags) => diff --git a/app/components/UI/Predict/types/flags.ts b/app/components/UI/Predict/types/flags.ts index 4490f87cbaac..112ffcb8ada6 100644 --- a/app/components/UI/Predict/types/flags.ts +++ b/app/components/UI/Predict/types/flags.ts @@ -56,6 +56,7 @@ export interface PredictFeatureFlags { predictHomepageDiscoveryNbaChampionEnabled: boolean; predictWorldCup: PredictWorldCupConfig; predictPortfolioEnabled: boolean; + predictHomeRedesignEnabled: boolean; } export interface PredictHotTabFlag extends VersionGatedFeatureFlag { diff --git a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts index ff9a93020d23..dce827338c87 100644 --- a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts +++ b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.test.ts @@ -33,6 +33,7 @@ describe('resolvePredictFeatureFlags', () => { predictWithAnyTokenEnabled: false, predictUpDownEnabled: false, predictPortfolioEnabled: false, + predictHomeRedesignEnabled: false, predictHomepageDiscoveryNbaChampionEnabled: true, predictWorldCup: DEFAULT_PREDICT_WORLD_CUP_FLAG, }); @@ -452,6 +453,134 @@ describe('resolvePredictFeatureFlags', () => { }); }); + describe('predictHomeRedesignEnabled', () => { + it('returns false when flag is missing', () => { + const result = resolvePredictFeatureFlags({}); + + expect(result.predictHomeRedesignEnabled).toBe(false); + }); + + it('returns true when enabled and version gate passes', () => { + mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => { + if ( + flag && + typeof flag === 'object' && + 'minimumVersion' in flag && + !('leagues' in flag) && + !('seriesId' in flag) + ) { + return true; + } + return undefined; + }); + + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + minimumVersion: '1.0.0', + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(true); + }); + + it('returns false when flag is disabled', () => { + mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => { + if ( + flag && + typeof flag === 'object' && + 'minimumVersion' in flag && + !('leagues' in flag) && + !('seriesId' in flag) + ) { + return false; + } + return undefined; + }); + + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: false, + minimumVersion: '1.0.0', + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(false); + }); + + it('returns false when flag is malformed', () => { + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: 'true', + minimumVersion: '1.0.0', + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(false); + }); + + it('returns false when version gate fails', () => { + mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => { + if ( + flag && + typeof flag === 'object' && + 'minimumVersion' in flag && + !('leagues' in flag) && + !('seriesId' in flag) + ) { + return false; + } + return undefined; + }); + + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + enabled: true, + minimumVersion: '99.0.0', + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(false); + }); + + it('unwraps progressive rollout shape', () => { + mockValidatedVersionGatedFeatureFlag.mockImplementation((flag) => { + if ( + flag && + typeof flag === 'object' && + 'minimumVersion' in flag && + !('leagues' in flag) && + !('seriesId' in flag) + ) { + return true; + } + return undefined; + }); + + const result = resolvePredictFeatureFlags({ + remoteFeatureFlags: { + predictHomeRedesign: { + name: 'group-a', + value: { + enabled: true, + minimumVersion: '1.0.0', + }, + }, + }, + }); + + expect(result.predictHomeRedesignEnabled).toBe(true); + }); + }); + describe('extendedSportsMarketsLeagues', () => { it('returns empty array when flag is missing', () => { const result = resolvePredictFeatureFlags({}); diff --git a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts index 1c3238edcbc0..1945ab7ab626 100644 --- a/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts +++ b/app/components/UI/Predict/utils/resolvePredictFeatureFlags.ts @@ -102,6 +102,9 @@ export function resolvePredictFeatureFlags( const predictPortfolioEnabled = resolveVersionGatedBooleanFlag( flags.predictPortfolio, ); + const predictHomeRedesignEnabled = resolveVersionGatedBooleanFlag( + flags.predictHomeRedesign, + ); const predictHomepageDiscoveryNbaChampionEnabled = resolveVersionGatedBooleanFlag( flags.predictHomepageDiscoveryNbaChampionEnabled, @@ -129,6 +132,7 @@ export function resolvePredictFeatureFlags( predictWithAnyTokenEnabled, predictUpDownEnabled, predictPortfolioEnabled, + predictHomeRedesignEnabled, predictHomepageDiscoveryNbaChampionEnabled, predictWorldCup, }; diff --git a/app/components/UI/Ramp/Views/Checkout/Checkout.tsx b/app/components/UI/Ramp/Views/Checkout/Checkout.tsx index 1e78970249b2..1d809233edb1 100644 --- a/app/components/UI/Ramp/Views/Checkout/Checkout.tsx +++ b/app/components/UI/Ramp/Views/Checkout/Checkout.tsx @@ -29,8 +29,8 @@ import { protectWalletModalVisible } from '../../../../../actions/user'; import { useRampsOrders } from '../../hooks/useRampsOrders'; import { BottomSheet, - type BottomSheetRef, HeaderStandard, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import useRampsUnifiedV2Enabled from '../../hooks/useRampsUnifiedV2Enabled'; import { showV2OrderToast } from '../../utils/v2OrderToast'; @@ -54,6 +54,7 @@ import { extractHostname, type CloseSource, } from '../../utils/webviewFunnelAnalytics'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; interface CheckoutParams { url: string; @@ -520,6 +521,7 @@ const Checkout = () => { /* no-op until initialized */ }); const closeHeadlessOnUnmountRef = useRef<() => void>(() => undefined); + const surfaceClass = useElevatedSurface(); closeHeadlessOnUnmountRef.current = () => { if (!headlessSessionId || hasTerminatedHeadlessSessionRef.current) { return; @@ -584,6 +586,7 @@ const Checkout = () => { goBack={navigation.goBack} isFullscreen keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > {sharedHeader} @@ -618,6 +621,7 @@ const Checkout = () => { isFullscreen isInteractable={!Device.isAndroid()} keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > {sharedHeader} { goBack={navigation.goBack} isFullscreen keyboardAvoidingViewEnabled={false} + twClassName={surfaceClass} > {sharedHeader} diff --git a/app/components/UI/Ramp/Views/Modals/ErrorDetailsModal/ErrorDetailsModal.tsx b/app/components/UI/Ramp/Views/Modals/ErrorDetailsModal/ErrorDetailsModal.tsx index 5d2329bddb5c..b661da85573b 100644 --- a/app/components/UI/Ramp/Views/Modals/ErrorDetailsModal/ErrorDetailsModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/ErrorDetailsModal/ErrorDetailsModal.tsx @@ -5,18 +5,18 @@ import { useNavigation, type ParamListBase } from '@react-navigation/native'; import type { StackNavigationProp } from '@react-navigation/stack'; import { BottomSheet, - type BottomSheetRef, - Text, - TextVariant, - TextColor, Button, - ButtonVariant, ButtonBaseSize, + ButtonVariant, HeaderStandard, Icon, + IconColor, IconName, IconSize, - IconColor, + Text, + TextColor, + TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { useStyles } from '../../../../../hooks/useStyles'; import { @@ -27,6 +27,7 @@ import Routes from '../../../../../../constants/navigation/Routes'; import { strings } from '../../../../../../../locales/i18n'; import Logger from '../../../../../../util/Logger'; import styleSheet from './ErrorDetailsModal.styles'; +import { useElevatedSurface } from '../../../../../../util/theme/themeUtils'; export interface ErrorDetailsModalParams { errorMessage: string; @@ -57,6 +58,7 @@ function ErrorDetailsModal() { showChangeProvider, amount, } = useParams(); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -91,7 +93,11 @@ function ErrorDetailsModal() { }, [navigation, amount]); return ( - + { trackEvent( @@ -256,7 +258,11 @@ function PaymentSelectionModal() { }; return ( - + (); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -96,7 +98,11 @@ function ProcessingInfoModal() { ]); return ( - + { @@ -180,6 +182,7 @@ function ProviderSelectionModal() { ref={sheetRef} goBack={navigation.goBack} onClose={handleDismiss} + twClassName={surfaceClass} > (false); @@ -196,7 +197,11 @@ function SettingsModal() { }, []); return ( - + @@ -160,7 +161,11 @@ function StateSelectorModal() { }, [scrollToTop]); return ( - + sheetRef.current?.onCloseBottomSheet()} diff --git a/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx b/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx index a110e10b27cd..09a20830fcb0 100644 --- a/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/TokenNotAvailableModal/TokenNotAvailableModal.tsx @@ -3,14 +3,14 @@ import { View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { BottomSheet, - type BottomSheetRef, - Text, - TextVariant, - TextColor, Button, - ButtonVariant, ButtonBaseSize, + ButtonVariant, HeaderStandard, + Text, + TextColor, + TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { strings } from '../../../../../../../locales/i18n'; import { @@ -26,8 +26,8 @@ import { createProviderSelectionModalNavigationDetails } from '../ProviderSelect import { useAnalytics } from '../../../../../hooks/useAnalytics/useAnalytics'; import { MetaMetricsEvents } from '../../../../../../core/Analytics'; import { TOKEN_NOT_AVAILABLE_MODAL_TEST_IDS } from './TokenNotAvailableModal.testIds'; - import type { BuyFlowOrigin } from '../../BuildQuote/BuildQuote'; +import { useElevatedSurface } from '../../../../../../util/theme/themeUtils'; export interface TokenNotAvailableModalParams { assetId: string; @@ -50,6 +50,7 @@ function TokenNotAvailableModal() { const { selectedProvider } = useRampsProviders(); const { selectedToken } = useRampsTokens(); + const surfaceClass = useElevatedSurface(); const tokenName = selectedToken?.name ?? ''; const providerName = selectedProvider?.name ?? ''; @@ -161,6 +162,7 @@ function TokenNotAvailableModal() { goBack={navigation.goBack} onClose={handleDismiss} testID={TOKEN_NOT_AVAILABLE_MODAL_TEST_IDS.MODAL} + twClassName={surfaceClass} > (); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const closeBottomSheetAndNavigate = useCallback( (navigateFunc: () => void) => { @@ -81,6 +81,7 @@ function UnsupportedStateModal() { ref={sheetRef} goBack={navigation.goBack} isInteractable={false} + twClassName={surfaceClass} > diff --git a/app/components/UI/Ramp/Views/Modals/UnsupportedTokenModal/UnsupportedTokenModal.tsx b/app/components/UI/Ramp/Views/Modals/UnsupportedTokenModal/UnsupportedTokenModal.tsx index 19be6b422754..e18678aa9ea6 100644 --- a/app/components/UI/Ramp/Views/Modals/UnsupportedTokenModal/UnsupportedTokenModal.tsx +++ b/app/components/UI/Ramp/Views/Modals/UnsupportedTokenModal/UnsupportedTokenModal.tsx @@ -4,16 +4,17 @@ import { useNavigation } from '@react-navigation/native'; import { BottomSheet, - type BottomSheetRef, HeaderStandard, Text, TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { strings } from '../../../../../../../locales/i18n'; import styleSheet from './UnsupportedTokenModal.styles'; import { useStyles } from '../../../../../hooks/useStyles'; import { createNavigationDetails } from '../../../../../../util/navigation/navUtils'; import Routes from '../../../../../../constants/navigation/Routes'; +import { useElevatedSurface } from '../../../../../../util/theme/themeUtils'; export const createUnsupportedTokenModalNavigationDetails = createNavigationDetails( @@ -25,9 +26,14 @@ function UnsupportedTokenModal() { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); return ( - + sheetRef.current?.onCloseBottomSheet()} diff --git a/app/components/UI/Ramp/components/EligibilityFailedModal/EligibilityFailedModal.tsx b/app/components/UI/Ramp/components/EligibilityFailedModal/EligibilityFailedModal.tsx index 7665cc1ba3b4..d127592438f5 100644 --- a/app/components/UI/Ramp/components/EligibilityFailedModal/EligibilityFailedModal.tsx +++ b/app/components/UI/Ramp/components/EligibilityFailedModal/EligibilityFailedModal.tsx @@ -4,15 +4,14 @@ import { useNavigation } from '@react-navigation/native'; import { BottomSheet, BottomSheetHeader, - type BottomSheetRef, - Text, - TextVariant, - TextColor, Button, ButtonSize, ButtonVariant, + Text, + TextColor, + TextVariant, + type BottomSheetRef, } from '@metamask/design-system-react-native'; - import styleSheet from './EligibilityFailedModal.styles'; import { useStyles } from '../../../../hooks/useStyles'; import { createNavigationDetails } from '../../../../../util/navigation/navUtils'; @@ -20,6 +19,7 @@ import Routes from '../../../../../constants/navigation/Routes'; import { strings } from '../../../../../../locales/i18n'; import { ELIGIBILITY_FAILED_MODAL_TEST_IDS } from './EligibilityFailedModal.testIds'; import { METAMASK_SUPPORT_URL } from '../../../../../constants/urls'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; const SUPPORT_URL = METAMASK_SUPPORT_URL; @@ -33,6 +33,7 @@ function EligibilityFailedModal() { const sheetRef = useRef(null); const navigation = useNavigation(); const { styles } = useStyles(styleSheet, {}); + const surfaceClass = useElevatedSurface(); const navigateToContactSupport = useCallback(() => { Linking.openURL(SUPPORT_URL).catch((error: unknown) => { @@ -50,6 +51,7 @@ function EligibilityFailedModal() { goBack={navigation.goBack} isInteractable={false} testID={ELIGIBILITY_FAILED_MODAL_TEST_IDS.MODAL} + twClassName={surfaceClass} > (null); const navigation = useNavigation(); + const surfaceClass = useElevatedSurface(); const handleClose = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -38,6 +39,7 @@ function RampUnsupportedModal() { goBack={navigation.goBack} isInteractable={false} testID={RAMP_UNSUPPORTED_MODAL_TEST_IDS.MODAL} + twClassName={surfaceClass} > = ({ const { trackEvent, createEventBuilder } = useAnalytics(); const { optInToCampaign, isOptingIn, optInError } = useOptInToCampaign(); const { showToast, RewardsToastOptions } = useRewardsToast(); + const surfaceClass = useElevatedSurface(); const handleOptIn = useCallback(async () => { try { @@ -76,7 +78,7 @@ const CampaignOptInSheet: React.FC = ({ ]); return ( - + {/* Header: centered title + close button */} = ({ }) => { const selectedGroup = useSelector(selectResolvedSelectedAccountGroup); const { height: screenHeight } = useWindowDimensions(); + const surfaceClass = useElevatedSurface(); const listStyle = useMemo( () => ({ maxHeight: screenHeight * 0.4 }), [screenHeight], ); return ( - + sheetRef.current?.onCloseBottomSheet(onClose)} > diff --git a/app/components/UI/Rewards/components/Campaigns/OndoAfterHoursSheet.tsx b/app/components/UI/Rewards/components/Campaigns/OndoAfterHoursSheet.tsx index 522f16b58208..1327a060d147 100644 --- a/app/components/UI/Rewards/components/Campaigns/OndoAfterHoursSheet.tsx +++ b/app/components/UI/Rewards/components/Campaigns/OndoAfterHoursSheet.tsx @@ -1,11 +1,11 @@ import React from 'react'; import { + BottomSheet, Box, BoxAlignItems, + BoxBackgroundColor, BoxFlexDirection, BoxJustifyContent, - BoxBackgroundColor, - BottomSheet, Button, ButtonIcon, ButtonSize, @@ -21,6 +21,7 @@ import { } from '@metamask/design-system-react-native'; import { strings } from '../../../../../../locales/i18n'; import { formatTimeRemaining } from '../../utils/formatUtils'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; interface OndoAfterHoursSheetProps { onClose: () => void; @@ -35,8 +36,10 @@ const OndoAfterHoursSheet: React.FC = ({ }) => { const countdownText = nextOpenAt ? formatTimeRemaining(nextOpenAt) : null; + const surfaceClass = useElevatedSurface(); + return ( - + {/* Header row: spacer + close button */} = ({ onClose, onConfirm, -}) => ( - - - {/* Close button */} - - - +}) => { + const surfaceClass = useElevatedSurface(); - {/* Warning icon */} - - - - - {/* Title */} - - + + {/* Close button */} + - {strings('rewards.ondo_campaign_not_eligible.title')} - - + + - {/* Body */} - - - {strings('rewards.ondo_campaign_not_eligible.body', { - days: ONDO_GM_REQUIRED_QUALIFIED_DAYS, - })} - - + {/* Warning icon */} + + + - {/* Buttons */} - - - + {/* Title */} + + + {strings('rewards.ondo_campaign_not_eligible.title')} + + + + {/* Body */} + + + {strings('rewards.ondo_campaign_not_eligible.body', { + days: ONDO_GM_REQUIRED_QUALIFIED_DAYS, + })} + + + + {/* Buttons */} + + + + - - -); + + ); +}; export default OndoNotEligibleSheet; diff --git a/app/components/UI/TokenDetails/components/RwaUnavailableBottomSheet/RwaUnavailableBottomSheet.tsx b/app/components/UI/TokenDetails/components/RwaUnavailableBottomSheet/RwaUnavailableBottomSheet.tsx index 79c5af2219a6..32aa7ebb9cd4 100644 --- a/app/components/UI/TokenDetails/components/RwaUnavailableBottomSheet/RwaUnavailableBottomSheet.tsx +++ b/app/components/UI/TokenDetails/components/RwaUnavailableBottomSheet/RwaUnavailableBottomSheet.tsx @@ -13,14 +13,15 @@ import { BottomSheetHeader, BottomSheetRef, Box, - Text, - TextVariant, BoxAlignItems, BoxJustifyContent, + Text, + TextVariant, } from '@metamask/design-system-react-native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; import { useNavigation } from '@react-navigation/native'; import { strings } from '../../../../../../locales/i18n'; +import { useElevatedSurface } from '../../../../../util/theme/themeUtils'; const ONDO_ELIGIBILITY_URL = 'https://docs.ondo.finance/ondo-global-markets/eligibility'; @@ -42,6 +43,7 @@ const RwaUnavailableBottomSheet = forwardRef< const [isVisible, setIsVisible] = useState(false); const tw = useTailwind(); const navigation = useNavigation(); + const surfaceClass = useElevatedSurface(); const handleSheetClosed = useCallback(() => { setIsVisible(false); @@ -99,7 +101,12 @@ const RwaUnavailableBottomSheet = forwardRef< } return ( - + {strings('rwa.unavailable.title')} diff --git a/app/components/Views/AddressSelector/AddressSelector.tsx b/app/components/Views/AddressSelector/AddressSelector.tsx index ed5392bcadab..cc869d414d69 100644 --- a/app/components/Views/AddressSelector/AddressSelector.tsx +++ b/app/components/Views/AddressSelector/AddressSelector.tsx @@ -9,12 +9,12 @@ import { AddressSelectorParams } from './AddressSelector.types'; import { AccountGroupId } from '@metamask/account-api'; import { BottomSheet, - type BottomSheetRef, BottomSheetHeader, Box, BoxAlignItems, BoxFlexDirection, BoxJustifyContent, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { toEvmCaipChainId } from '@metamask/multichain-network-controller'; import { isCaipChainId } from '@metamask/utils'; @@ -40,6 +40,7 @@ import { createAccountSelectorNavDetails } from '../AccountSelector'; import { NetworkConfiguration } from '@metamask/network-controller'; import { strings } from '../../../../locales/i18n'; import { AddressSelectorSelectors } from './AddressSelector.testIds'; +import { useElevatedSurface } from '../../../util/theme/themeUtils'; export const createAddressSelectorNavDetails = createNavigationDetails( @@ -72,6 +73,7 @@ const AddressSelector = () => { ); const accountName = useAccountName(); + const surfaceClass = useElevatedSurface(); const selectedCaipChainId = isCaipChainId(selectedChainId) ? selectedChainId : toEvmCaipChainId(selectedChainId); @@ -151,7 +153,12 @@ const AddressSelector = () => { }, [internalAccountsSpreadByScopes, isEvmOnly, displayOnlyCaipChainIds]); return ( - + sheetRef.current?.onCloseBottomSheet()}> {strings('address_selector.select_an_address')} diff --git a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx index daad79080bb7..adb83c249ec4 100644 --- a/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx +++ b/app/components/Views/MultichainAccounts/IntroModal/LearnMoreBottomSheet.tsx @@ -1,16 +1,16 @@ import React, { useState, useCallback, useRef } from 'react'; import { View } from 'react-native'; import { - Text, + BottomSheet, + Button, + ButtonBaseSize, ButtonIcon, + ButtonVariant, Checkbox, - TextVariant, IconName, + Text, TextColor, - Button, - ButtonVariant, - ButtonBaseSize, - BottomSheet, + TextVariant, type BottomSheetRef, } from '@metamask/design-system-react-native'; import { useNavigation, useTheme } from '@react-navigation/native'; @@ -22,6 +22,7 @@ import { RootState } from '../../../../reducers'; import { useDispatch, useSelector } from 'react-redux'; import { setMultichainAccountsIntroModalSeen } from '../../../../actions/user'; import { LEARN_MORE_BOTTOM_SHEET_TEST_IDS } from './LearnMoreBottomSheet.testIds'; +import { useElevatedSurface } from '../../../../util/theme/themeUtils'; interface LearnMoreBottomSheetProps { onClose?: () => void; @@ -39,6 +40,7 @@ const LearnMoreBottomSheet: React.FC = ({ const isBasicFunctionalityEnabled = useSelector( (state: RootState) => state?.settings?.basicFunctionalityEnabled, ); + const surfaceClass = useElevatedSurface(); const handleBack = useCallback(() => { sheetRef.current?.onCloseBottomSheet(); @@ -66,6 +68,7 @@ const LearnMoreBottomSheet: React.FC = ({ ref={sheetRef} onClose={onClose} testID={LEARN_MORE_BOTTOM_SHEET_TEST_IDS.BOTTOM_SHEET} + twClassName={surfaceClass} > diff --git a/app/components/Views/NetworksManagement/components/DeleteNetworkModal.tsx b/app/components/Views/NetworksManagement/components/DeleteNetworkModal.tsx index 21baec712bc4..8c48e1707502 100644 --- a/app/components/Views/NetworksManagement/components/DeleteNetworkModal.tsx +++ b/app/components/Views/NetworksManagement/components/DeleteNetworkModal.tsx @@ -1,19 +1,20 @@ import React, { forwardRef } from 'react'; import { - Box, BottomSheet, BottomSheetFooter, BottomSheetHeader, - type BottomSheetRef, + Box, + BoxAlignItems, ButtonSize, Text, TextVariant, - BoxAlignItems, + type BottomSheetRef, } from '@metamask/design-system-react-native'; import { strings } from '../../../../../locales/i18n'; import { NetworksManagementViewSelectorsIDs } from '../NetworksManagementView.testIds'; +import { useElevatedSurface } from '../../../../util/theme/themeUtils'; interface DeleteNetworkModalProps { networkName: string; @@ -37,12 +38,15 @@ const DeleteNetworkModal = forwardRef( testID: NetworksManagementViewSelectorsIDs.DELETE_CONFIRM_BUTTON, }; + const surfaceClass = useElevatedSurface(); + return ( {`${strings('app_settings.delete')} ${networkName} ${strings('app_settings.network')}`} diff --git a/app/components/Views/OnboardingSheet/index.tsx b/app/components/Views/OnboardingSheet/index.tsx index c77ef589989d..cbf0dab43988 100644 --- a/app/components/Views/OnboardingSheet/index.tsx +++ b/app/components/Views/OnboardingSheet/index.tsx @@ -2,6 +2,8 @@ import React, { useRef } from 'react'; import { strings } from '../../../../locales/i18n'; import { useTheme } from '../../../util/theme'; import { AppThemeKey } from '../../../util/theme/models'; +import { useElevatedSurface } from '../../../util/theme/themeUtils'; + import GoogleIcon from 'images/google.svg'; import AppleIcon from 'images/apple.svg'; import AppleWhiteIcon from 'images/apple-white.svg'; @@ -57,7 +59,6 @@ const OnboardingSheet = () => { } = params ?? {}; const { colors } = useTheme(); const tw = useTailwind(); - const onPressCreateAction = () => { if (onPressCreate) { onPressCreate(); @@ -109,10 +110,15 @@ const OnboardingSheet = () => { }; const { themeAppearance } = useTheme(); + const surfaceClass = useElevatedSurface(); const isDark = themeAppearance === AppThemeKey.dark; return ( - + { const bottomSheetRef = useRef(null); const navigation = useNavigation(); - + const surfaceClass = useElevatedSurface(); const goBack = useCallback(() => { goBackIfFocused(navigation); }, [navigation]); return ( - + = ({ const [isContentReady, setIsContentReady] = useState(false); const [activeScreen, setActiveScreen] = useState('amount'); const isSubmittingTx = useSelector(selectIsSubmittingTx); + const surfaceClass = useElevatedSurface(); useEffect(() => { bottomSheetRef.current?.onOpenBottomSheet(() => { @@ -82,6 +84,7 @@ const QuickBuyRootInner: React.FC = ({ ref={bottomSheetRef} isInteractable={!isSubmittingTx} onClose={onClose} + twClassName={surfaceClass} > {isContentReady ? ( = ({ }) => { const [isModalVisible, setIsModalVisible] = useState(false); const bottomSheetRef = useRef(null); - + const surfaceClass = useElevatedSurface(); const { styles } = useStyles(stylesheet, {}); const internalAccountsById = useSelector(selectInternalAccountsById); @@ -209,6 +211,7 @@ const AccountSelector: React.FC = ({ isFullscreen keyboardAvoidingViewEnabled={false} onClose={handleSheetClosed} + twClassName={surfaceClass} > {title} diff --git a/app/components/Views/confirmations/components/send/send-alert-modal/send-alert-modal.tsx b/app/components/Views/confirmations/components/send/send-alert-modal/send-alert-modal.tsx index 7a3734b7fc3d..f962e02eb2b5 100644 --- a/app/components/Views/confirmations/components/send/send-alert-modal/send-alert-modal.tsx +++ b/app/components/Views/confirmations/components/send/send-alert-modal/send-alert-modal.tsx @@ -1,12 +1,12 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Modal } from 'react-native'; import { + BottomSheet, + BottomSheetRef, Box, BoxAlignItems, BoxFlexDirection, BoxJustifyContent, - BottomSheet, - BottomSheetRef, ButtonIcon, Icon, IconColor, @@ -24,6 +24,7 @@ import { } from '../../../../../../component-library/components/Buttons/Button'; import type { SendAlert } from '../../../hooks/send/alerts/types'; import { SendAlertModalProps } from './send-alert-modal.types'; +import { useElevatedSurface } from '../../../../../../util/theme/themeUtils'; function PageNavigation({ alerts, @@ -82,6 +83,7 @@ export const SendAlertModal = ({ onClose, }: SendAlertModalProps) => { const bottomSheetRef = useRef(null); + const surfaceClass = useElevatedSurface(); const [currentIndex, setCurrentIndex] = useState(0); const alertKeys = alerts.map((a) => a.key).join('|'); @@ -124,7 +126,11 @@ export const SendAlertModal = ({ return ( - + { expect(mockResolveNativePushPermissionStatus).not.toHaveBeenCalled(); }); - it('returns the marketing consent prompt when OS push is enabled and Redux marketing consent is missing', async () => { + it('returns the marketing consent prompt when OS push is enabled and Redux marketing consent is not enabled', async () => { const { result } = renderUsePushPrePromptVariant(); await waitFor(() => { @@ -304,6 +308,28 @@ describe('usePushPrePromptVariant', () => { expect(result.current.nativeOsPermissionEnabled).toBe(true); }); + it('does not show the marketing consent prompt when marketing consent is turned off after startup resolution', async () => { + const { result, store } = renderUsePushPrePromptVariant({ + hasMarketingConsent: true, + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + expect(result.current.variant).toBeNull(); + expect(result.current.nativeOsPermissionEnabled).toBe(true); + + await act(async () => { + store.dispatch(setDataCollectionForMarketing(false)); + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + + expect(result.current.variant).toBeNull(); + }); + it('defers the marketing consent prompt while social login marketing consent backfill is pending', async () => { const { result } = renderUsePushPrePromptVariant({ pendingSocialLoginMarketingConsentBackfill: 'google', @@ -317,6 +343,48 @@ describe('usePushPrePromptVariant', () => { expect(mockResolveNativePushPermissionStatus).toHaveBeenCalledTimes(1); }); + it('returns the marketing consent prompt after social login marketing consent backfill clears without consent', async () => { + const { result, store } = renderUsePushPrePromptVariant({ + pendingSocialLoginMarketingConsentBackfill: 'google', + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + expect(result.current.variant).toBeNull(); + + await act(async () => { + store.dispatch(setPendingSocialLoginMarketingConsentBackfill(null)); + }); + + await waitFor(() => { + expect(result.current.variant).toBe('marketing_consent'); + }); + expect(result.current.nativeOsPermissionEnabled).toBe(true); + }); + + it('does not return the marketing consent prompt after social login marketing consent backfill clears with consent', async () => { + const { result, store } = renderUsePushPrePromptVariant({ + pendingSocialLoginMarketingConsentBackfill: 'google', + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + expect(result.current.variant).toBeNull(); + + await act(async () => { + store.dispatch(setDataCollectionForMarketing(true)); + store.dispatch(setPendingSocialLoginMarketingConsentBackfill(null)); + }); + + await waitFor(() => { + expect(result.current.isResolving).toBe(false); + }); + expect(result.current.variant).toBeNull(); + expect(result.current.nativeOsPermissionEnabled).toBe(true); + }); + it('does not defer the push permission prompt for social login marketing consent backfill', async () => { mockNativePushPermissionStatus({ nativeOsPermissionEnabled: false, diff --git a/app/util/notifications/hooks/usePushPrePromptVariant.ts b/app/util/notifications/hooks/usePushPrePromptVariant.ts index cd6c8dc85cbf..e961e9a25f23 100644 --- a/app/util/notifications/hooks/usePushPrePromptVariant.ts +++ b/app/util/notifications/hooks/usePushPrePromptVariant.ts @@ -31,6 +31,7 @@ interface PushPrePromptEligibility { canShowPrePrompt: boolean; hasPrePromptBeenShown: boolean; hasMarketingConsent: boolean; + isMarketingConsentResolutionPending: boolean; pendingSocialLoginMarketingConsentBackfill: string | null; } @@ -38,12 +39,14 @@ const getResolutionKey = ({ canShowPrePrompt, hasPrePromptBeenShown, hasMarketingConsent, + isMarketingConsentResolutionPending, pendingSocialLoginMarketingConsentBackfill, }: PushPrePromptEligibility) => [ `canShowPrePrompt:${canShowPrePrompt}`, `hasPrePromptBeenShown:${hasPrePromptBeenShown}`, `hasMarketingConsent:${hasMarketingConsent}`, + `isMarketingConsentResolutionPending:${isMarketingConsentResolutionPending}`, `pendingSocialLoginMarketingConsentBackfill:${ pendingSocialLoginMarketingConsentBackfill ?? 'null' }`, @@ -96,7 +99,10 @@ const resolvePrePromptVariant = async ( }; } - if (eligibility.pendingSocialLoginMarketingConsentBackfill) { + if ( + eligibility.isMarketingConsentResolutionPending || + eligibility.pendingSocialLoginMarketingConsentBackfill + ) { return { nativeOsPermissionEnabled, variant: null, @@ -149,6 +155,30 @@ export function usePushPrePromptVariant(): { selectPendingSocialLoginMarketingConsentBackfill, ); + const [startupMarketingConsent, setStartupMarketingConsent] = useState< + boolean | null + >(() => + pendingSocialLoginMarketingConsentBackfill ? null : hasMarketingConsent, + ); + + useEffect(() => { + if ( + startupMarketingConsent !== null || + pendingSocialLoginMarketingConsentBackfill + ) { + return; + } + + setStartupMarketingConsent(hasMarketingConsent); + }, [ + hasMarketingConsent, + pendingSocialLoginMarketingConsentBackfill, + startupMarketingConsent, + ]); + + const isMarketingConsentResolutionPending = startupMarketingConsent === null; + const startupHasMarketingConsent = startupMarketingConsent === true; + const canShowPrePrompt = Boolean(completedOnboarding) && isNotificationsFeatureAvailable && @@ -167,13 +197,15 @@ export function usePushPrePromptVariant(): { () => ({ canShowPrePrompt, hasPrePromptBeenShown, - hasMarketingConsent, + hasMarketingConsent: startupHasMarketingConsent, + isMarketingConsentResolutionPending, pendingSocialLoginMarketingConsentBackfill, }), [ canShowPrePrompt, hasPrePromptBeenShown, - hasMarketingConsent, + isMarketingConsentResolutionPending, + startupHasMarketingConsent, pendingSocialLoginMarketingConsentBackfill, ], ); diff --git a/app/util/theme/themeUtils.ts b/app/util/theme/themeUtils.ts index e7221d9cb8e0..759a82af168a 100644 --- a/app/util/theme/themeUtils.ts +++ b/app/util/theme/themeUtils.ts @@ -1,9 +1,18 @@ import { useTheme } from './index'; import { AppThemeKey, Theme } from './models'; -// When pure black is off, bg-section is the standard elevated surface color for -// both light and dark. When pure black is on, bg-default is overridden to -// #000000 so light mode still uses bg-default while dark mode uses bg-section. +// Stopgap surface helper for the MM_PURE_BLACK_PREVIEW rollout. +// +// When pure black is OFF, returns `bg-default` (current behavior — no change +// for normal light/dark mode users). +// +// When pure black is ON: +// - dark → `bg-section` (elevated `#1c1d1f` so surfaces don't collapse +// into the pure-black screen background) +// - light → `bg-default` (unchanged, light mode is unaffected) +// +// Remove these helpers once the MMDS package ships its own pure-black-aware +// surface tokens and the flag is enabled by default. export const isPureBlackEnabled = process.env.MM_PURE_BLACK_PREVIEW === 'true'; export const getElevatedSurfaceColor = (theme: Theme): string => { diff --git a/bitrise.yml b/bitrise.yml new file mode 100644 index 000000000000..17f707e0a19b --- /dev/null +++ b/bitrise.yml @@ -0,0 +1,3608 @@ +--- +format_version: '8' +default_step_lib_source: 'https://github.com/bitrise-io/bitrise-steplib.git' +project_type: react-native + +#Pipelines are listed below +pipelines: + # Generates prod builds for all targets + create_prod_builds_pipeline: + stages: + - create_prod_builds: {} + create_android_env_builds_pipeline: + stages: + - create_android_env_builds: {} + #Creates MetaMask-QA apps and stores apk/ipa in Bitrise + create_qa_builds_pipeline: + stages: + - create_build_qa: {} + #Builds MetaMask, MetaMask-QA apps and stores apk/ipa in Bitrise + build_all_targets_pipeline: + stages: + - bump_version_stage: {} + - create_build_release: {} + - bump_version_stage: {} + - create_build_beta: {} + - bump_version_stage: {} + - create_build_qa: {} + - bump_version_stage: {} + #Releases MetaMask apps and stores apk/ipa into Play(Internal Testing)/App(TestFlight) Store + release_builds_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_build_release: {} + - deploy_build_release: {} + - create_build_qa: {} #Generate QA builds for E2E app upgrade tests + #Releases MetaMask beta apps and stores apk/ipa into Play(Internal Testing)/App(TestFlight) Store + beta_builds_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_build_beta: {} + - deploy_build_release: {} + #Releases MetaMask RC apps for release buil profiling + release_rc_builds_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_build_rc: {} + - deploy_build_release: {} + #Releases MetaMask apps and stores ipa into App(TestFlight) Store + release_ios_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_ios_release: {} + - deploy_ios_release: {} + #Releases MetaMask apps and stores apk Play(Internal Testing) Store + release_android_to_store_pipeline: + stages: + - bump_version_stage: {} + - create_android_release: {} + - deploy_android_release: {} + # TODO: Remove this workflow since it's not used anymore + run_e2e_ios_pipeline: + stages: + - build_e2e_ios_stage: {} + - run_e2e_ios_stage: {} + - notify: {} + # TODO: Remove this workflow since it's not used anymore + run_e2e_flask_pipeline: + stages: + - pr_cache_check_stage: {} + - build_e2e_flask_ios_android: {} + - run_e2e_flask_ios_android: {} + - notify: {} + #Run single suits or test files + run_single_test_suite_ios_android: + stages: + - build_smoke_e2e_ios_android_stage: {} + - run_single_e2e_ios_android_stage: {} + - notify: {} + #Run E2E test suite for Android only + run_e2e_android_pipeline: + stages: + - build_e2e_android_stage: {} #builds android detox E2E + - run_e2e_android_stage: {} #runs android detox test E2E + - notify: {} + #PR_e2e_verfication (build ios & android), run iOS (smoke), emulator Android + release_e2e_pipeline: + stages: + - build_e2e_ios_android_stage: {} + - run_release_e2e_ios_android_stage: {} + - notify: {} + #PR_e2e_verfication (build ios & android), run iOS (smoke), emulator Android + pr_smoke_e2e_pipeline: + stages: + - set_main_target_stage: {} + - pr_cache_check_stage: {} + - build_smoke_e2e_ios_android_stage: {} + - run_smoke_e2e_ios_android_stage: {} + - notify: {} + + flask_smoke_e2e_pipeline: + stages: + - set_flask_target_stage: {} + - pr_cache_check_stage: {} + - build_e2e_flask_ios_android: {} + - run_e2e_flask_ios_android_stage: {} + - notify: {} + + #Performance smoke test pipeline - runs only performance tests + smoke_e2e_performance_pipeline: + stages: + - set_main_target_stage: {} + - pr_cache_check_stage: {} + - build_smoke_e2e_ios_android_stage: {} + # - run_smoke_e2e_performance_ios_android_stage: {} + - notify: {} + + #PR_e2e_verfication (build ios & android), run iOS (regression), emulator Android + pr_rc_rwy_pipeline: + workflows: + set_main_target_workflow: {} + pr_check_build_cache: + depends_on: + - set_main_target_workflow + abort_on_fail: true + build_ios_rc_and_upload_sourcemaps: + depends_on: + - pr_check_build_cache + build_android_rc_and_upload_sourcemaps: + depends_on: + - pr_check_build_cache + expo_dev_pipeline: + stages: + - create_build_dev_expo: {} + # - app_launch_times_test_stage: {} + #Main expo pipeline + expo_main_pipeline: + stages: + - create_build_main_expo: {} + #Flask expo pipeline + expo_flask_pipeline: + stages: + - create_build_flask_expo: {} + #QA expo pipeline + expo_qa_pipeline: + stages: + - create_build_qa_expo: {} + # multichain_permissions_e2e_pipeline: + # stages: + # - build_multichain_permissions_e2e_ios_android_stage: {} + # - run_multichain_permissions_e2e_ios_android_stage: {} + # Pipeline for Flask + create_flask_release_builds_pipeline: + stages: + - create_build_flask_release: {} + - notify: {} + release_flask_builds_to_store_pipeline: + stages: + - create_build_flask_release: {} + - deploy_flask_build_release: {} + - release_notify: {} + nightly_exp_builds_pipeline: + workflows: + bump_version_code: {} + build_android_main_exp: + depends_on: + - bump_version_code + build_ios_main_exp: + depends_on: + - bump_version_code + nightly_rc_builds_pipeline: + workflows: + bump_version_code: {} + build_android_main_rc: + depends_on: + - bump_version_code + build_ios_main_rc: + depends_on: + - bump_version_code + exp_builds_to_testflight_pipeline: + workflows: + bump_version_code: {} + build_android_main_exp: + depends_on: + - bump_version_code + build_ios_main_exp: + depends_on: + - bump_version_code + upload_ios_main_to_testflight: + depends_on: + - build_ios_main_exp + rc_builds_to_testflight_pipeline: + workflows: + bump_version_code: {} + build_android_main_rc: + depends_on: + - bump_version_code + build_ios_main_rc: + depends_on: + - bump_version_code + upload_ios_main_to_testflight: + depends_on: + - build_ios_main_rc +#Stages reference workflows. Those workflows cannot but utility "_this-is-a-utility" +stages: + bump_version_stage: + workflows: + - bump_version_code: {} + create_build_all_targets: + workflows: + - build_android_release: {} + - build_ios_release: {} + - build_android_flask_release: {} + - build_ios_flask_release: {} + - build_android_qa: {} + - build_ios_qa: {} + - build_android_devbuild: {} + - build_ios_devbuild: {} + - build_ios_simbuild: {} + create_build_release: + workflows: + - build_android_main_prod: {} + - build_ios_main_prod: {} + create_build_beta: + workflows: + - build_android_main_beta: {} + - build_ios_main_beta: {} + deploy_build_release: + workflows: + - deploy_android_to_store: {} + - deploy_ios_to_store: {} + create_ios_release: + workflows: + - build_ios_release: {} + deploy_ios_release: + workflows: + - deploy_ios_to_store: {} + create_android_release: + workflows: + - build_android_main_prod: {} + create_android_release_new: + workflows: + - build_android_main_prod: {} + - build_android_main_beta: {} + - build_android_main_rc: {} + create_ios_release_new: + workflows: + - build_ios_main_prod: {} + - build_ios_main_beta: {} + - build_ios_main_rc: {} + create_build_rc: + workflows: + - build_android_main_rc: {} + - build_ios_main_rc: {} + deploy_android_release: + workflows: + - deploy_android_to_store: {} + create_build_dev_expo: + workflows: + - build_android_devbuild: {} + - build_ios_devbuild: {} + - build_ios_simbuild: {} + create_build_main_expo: + workflows: + - build_android_devbuild: {} + - build_ios_devbuild: {} + - build_ios_simbuild: {} + create_build_flask_expo: + workflows: + - build_android_flask_devbuild: {} + - build_ios_flask_devbuild: {} + - build_ios_flask_simbuild: {} + create_build_qa_expo: + workflows: + - build_android_qa_devbuild: {} + - build_ios_qa_devbuild: {} + - build_ios_qa_simbuild: {} + create_build_qa: + workflows: + - build_android_qa: {} + - build_ios_qa: {} + create_prod_builds: + workflows: + - build_ios_main_prod: {} + - build_android_main_prod: {} + - build_ios_qa_prod: {} + - build_android_qa_prod: {} + - build_ios_flask_prod: {} + - build_android_flask_prod: {} + create_android_env_builds: + workflows: + - build_android_main_prod: {} + - build_android_main_rc: {} + - build_android_main_beta: {} + - build_android_main_exp: {} + - build_android_main_test: {} + - build_android_main_e2e: {} + - build_android_main_dev: {} + - build_android_flask_prod: {} + - build_android_flask_test: {} + - build_android_flask_e2e: {} + - build_android_flask_dev: {} + create_build_qa_android: + workflows: + - build_android_qa: {} + create_build_qa_ios: + workflows: + - build_ios_qa: {} + # TODO: Remove this workflow since it's not used anymore + build_e2e_ios_stage: + workflows: + - ios_e2e_build: {} + # TODO: Remove this workflow since it's not used anymore + run_e2e_ios_stage: + workflows: + - ios_e2e_test: {} + pr_cache_check_stage: + abort_on_fail: true + workflows: + - pr_check_build_cache: {} + # Sets the METAMASK_BUILD_TYPE variable to the main target + set_main_target_stage: + workflows: + - set_main_target_workflow: {} + # Sets the METAMASK_BUILD_TYPE variable to the flask target + set_flask_target_stage: + workflows: + - set_flask_target_workflow: {} + build_smoke_e2e_ios_android_stage: + abort_on_fail: true + workflows: + - build_ios_main_e2e: + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + # Disabling in CI to allow GHA runs + # - build_android_main_e2e: + # run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "false"}}' + build_multichain_permissions_e2e_ios_android_stage: + abort_on_fail: true + workflows: + - build_ios_multichain_permissions_e2e: {} + - build_android_multichain_permissions_e2e: {} + # run_multichain_permissions_e2e_ios_android_stage: + # workflows: + # - run_tag_multichain_permissions_ios: {} + # - run_tag_multichain_permissions_android: {} + run_e2e_flask_ios_android_stage: + workflows: + - run_ios_api_specs: {} + - run_trade_swimlane_ios_smoke: {} + - run_trade_swimlane_android_smoke: {} + - run_network_abstraction_swimlane_ios_smoke: {} + - run_network_abstraction_swimlane_android_smoke: {} + - run_network_expansion_swimlane_ios_smoke: {} + - run_network_expansion_swimlane_android_smoke: {} + - run_wallet_platform_swimlane_ios_smoke: {} + - run_wallet_platform_swimlane_android_smoke: {} + - run_tag_smoke_confirmations_android: {} + - run_tag_smoke_confirmations_ios: {} + - run_tag_flask_build_tests_ios: {} + - run_tag_flask_build_tests_android: {} + - run_tag_smoke_accounts_ios: {} + - run_tag_smoke_accounts_android: {} + run_single_e2e_ios_android_stage: + workflows: + - run_single_ios_e2e_test: {} + - run_single_android_e2e_test: {} + run_smoke_e2e_ios_android_stage: + workflows: + - run_ios_api_specs: {} + - run_trade_swimlane_ios_smoke: {} + # - run_trade_swimlane_android_smoke: {} + - run_network_abstraction_swimlane_ios_smoke: {} + # - run_network_abstraction_swimlane_android_smoke: {} + - run_network_expansion_swimlane_ios_smoke: {} + # - run_network_expansion_swimlane_android_smoke: {} + - run_wallet_platform_swimlane_ios_smoke: {} + # - run_wallet_platform_swimlane_android_smoke: {} + # - run_tag_smoke_confirmations_android: {} + - run_tag_smoke_identity_ios: {} + # - run_tag_smoke_identity_android: {} + - run_tag_smoke_confirmations_ios: {} + - run_tag_smoke_multichain_api_ios: {} + - run_tag_smoke_accounts_ios: {} + # - run_tag_smoke_accounts_android: {} + # - run_tag_smoke_performance_ios: {} + # - run_tag_smoke_performance_android: {} + - run_tag_smoke_money_ios: {} + # - run_tag_smoke_money_android: {} + # The entire workflow is disabled as Android runs on GHA and iOS was already skipped due to a regression + # run_smoke_e2e_performance_ios_android_stage: + # workflows: + # - run_tag_smoke_performance_ios: {} + #- run_tag_smoke_performance_android: {} + # TODO: This stage does the same thing as build_smoke_e2e_ios_android_stage + build_regression_e2e_ios_android_stage: + abort_on_fail: true + workflows: + - build_ios_main_e2e: + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + - build_android_main_e2e: + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "false"}}' + run_regression_e2e_ios_android_stage: + workflows: + - ios_run_regression_confirmations_tests: {} + - ios_run_regression_wallet_platform_tests: {} + - ios_run_regression_trade_tests: {} + - ios_run_regression_network_abstraction_tests: {} + - ios_run_regression_network_expansion_tests: {} + - ios_run_regression_accounts_tests: {} + - ios_run_regression_ux_tests: {} + - ios_run_regression_assets_tests: {} + - android_run_regression_confirmations_tests: {} + - android_run_regression_wallet_platform_tests: {} + - android_run_regression_trade_tests: {} + - android_run_regression_network_abstraction_tests: {} + - android_run_regression_network_expansion_tests: {} + - android_run_regression_performance_tests: {} + - android_run_regression_assets_tests: {} + - android_run_regression_accounts_tests: {} + - android_run_regression_ux_tests: {} + run_release_e2e_ios_android_stage: + workflows: + - ios_run_regression_confirmations_tests: {} + - ios_run_regression_wallet_platform_tests: {} + - ios_run_regression_trade_tests: {} + - ios_run_regression_network_abstraction_tests: {} + - ios_run_regression_network_expansion_tests: {} + - ios_run_regression_accounts_tests: {} + - ios_run_regression_ux_tests: {} + - ios_run_regression_assets_tests: {} + - android_run_regression_confirmations_tests: {} + - android_run_regression_wallet_platform_tests: {} + - android_run_regression_trade_tests: {} + - android_run_regression_network_abstraction_tests: {} + - android_run_regression_network_expansion_tests: {} + - android_run_regression_performance_tests: {} + - android_run_regression_assets_tests: {} + - android_run_regression_accounts_tests: {} + - android_run_regression_ux_tests: {} + - run_ios_api_specs: {} + - run_trade_swimlane_ios_smoke: {} + - run_trade_swimlane_android_smoke: {} + - run_network_expansion_swimlane_ios_smoke: {} + - run_network_expansion_swimlane_android_smoke: {} + - run_network_abstraction_swimlane_ios_smoke: {} + - run_network_abstraction_swimlane_android_smoke: {} + - run_wallet_platform_swimlane_ios_smoke: {} + - run_wallet_platform_swimlane_android_smoke: {} + - run_tag_smoke_confirmations_android: {} + - run_tag_smoke_confirmations_ios: {} + - run_tag_smoke_multichain_api_ios: {} + - run_tag_smoke_accounts_ios: {} + - run_tag_smoke_accounts_android: {} + - run_tag_smoke_money_ios: {} + - run_tag_smoke_money_android: {} + build_regression_e2e_ios_gns_disabled_stage: + abort_on_fail: true + workflows: + - build_ios_main_e2e_gns_disabled: + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + run_regression_e2e_ios_gns_disabled_stage: + workflows: + - ios_run_regression_network_abstraction_tests_gns_disabled: {} + + # TODO: Remove this stage since it's not used anymore + run_e2e_ios_android_stage: + workflows: + - ios_e2e_test: {} + - android_e2e_test: {} + build_e2e_ios_android_stage: + workflows: + - build_android_qa: {} + - ios_e2e_build: {} + - android_e2e_build: {} + build_e2e_android_stage: + workflows: + - android_e2e_build: {} + run_e2e_android_stage: + workflows: + - android_e2e_test: {} + notify: + workflows: + - notify_success: {} + release_notify: + workflows: + - release_announcing_stores: {} + build_e2e_flask_ios_android: + workflows: + - build_android_flask_e2e: {} + - build_ios_flask_e2e: {} + # TODO: Remove this workflow since it's not used anymore + run_e2e_flask_ios_android: + workflows: + - run_flask_e2e_android: {} + - run_flask_e2e_ios: {} + create_build_flask_release: + workflows: + - build_android_flask_release: {} + - build_ios_flask_release: {} + deploy_flask_build_release: + workflows: + - deploy_android_to_store: + envs: + - MM_ANDROID_PACKAGE_NAME: 'io.metamask.flask' + - deploy_ios_to_store: + +workflows: + # Code Setups + setup: + steps: + - activate-ssh-key@4: + run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' + - git-clone@6: {} + set_commit_hash: + steps: + - script@1: + title: Set commit hash env variable + inputs: + - content: |- + #!/usr/bin/env bash + BRANCH_COMMIT_HASH="$(git rev-parse HEAD)" + + # Log the value of BRANCH_COMMIT_HASH + echo "BRANCH_COMMIT_HASH is set to: $BRANCH_COMMIT_HASH" + + envman add --key BRANCH_COMMIT_HASH --value "$BRANCH_COMMIT_HASH" + - share-pipeline-variable@1: + title: Persist commit hash across all stages + inputs: + - variables: |- + BRANCH_COMMIT_HASH + code_setup: + before_run: + - setup + - prep_environment + steps: + # - restore-cocoapods-cache@2: {} + - yarn@0: + inputs: + - command: install --immutable + title: Yarn Install + - script@1: + inputs: + - content: |- + #!/usr/bin/env bash + envman add --key METAMASK_YARN_CACHE_DIR --value "$(yarn cache dir)" + title: Get Yarn cache directory + - yarn@0: + inputs: + - command: setup:github-ci + title: Yarn Setup + prep_environment: + steps: + - restore-cache@2: + title: Restore Node + inputs: + - key: node-{{ getenv "NODE_VERSION" }}-{{ .OS }}-{{ .Arch }} + - script@1: + title: node, yarn, corepack installation + inputs: + - content: |- + #!/usr/bin/env bash + echo "Gems being installed with bundler gem" + bundle install --gemfile=ios/Gemfile + echo "Node $NODE_VERSION being installed" + + set -e + + # Add and enable NVM + wget -O install-nvm.sh "https://raw.githubusercontent.com/nvm-sh/nvm/v${NVM_VERSION}/install.sh" + echo "${NVM_SHA256SUM} install-nvm.sh" > install-nvm.sh.SHA256SUM + sha256sum -c install-nvm.sh.SHA256SUM + chmod +x install-nvm.sh && ./install-nvm.sh && rm ./install-nvm.sh + source "${HOME}/.nvm/nvm.sh" + echo 'source "${HOME}/.nvm/nvm.sh"' | tee -a ${HOME}/.{bashrc,profile} + + # Retry logic for Node installation + MAX_ATTEMPTS=3 + ATTEMPT=1 + until [ $ATTEMPT -gt $MAX_ATTEMPTS ] + do + echo "Attempt $ATTEMPT to install Node.js" + nvm install ${NODE_VERSION} + INSTALL_STATUS=$? # Capture the exit status of the nvm install command + if [ $INSTALL_STATUS -eq 0 ]; then + echo "Node.js installation successful!" + break + else + echo "Node.js installation failed with exit code $INSTALL_STATUS" + ATTEMPT=$((ATTEMPT+1)) + echo "Node.js installation failed, retrying in 5 seconds..." + sleep 5 + fi + done + + if [ $ATTEMPT -gt $MAX_ATTEMPTS ]; then + echo "Node.js installation failed after $MAX_ATTEMPTS attempts." + exit 1 + fi + envman add --key PATH --value $PATH + + node --version + + echo "Corepack being installed with npm" + npm i -g "corepack@$COREPACK_VERSION" + echo "Corepack enabling $YARN_VERSION" + corepack enable + - save-cache@1: + title: Save Node + inputs: + - key: node-{{ getenv "NODE_VERSION" }}-{{ .OS }}-{{ .Arch }} + - paths: |- + ../.nvm/ + ../../../root/.nvm/ + extract_version_info: + steps: + - script@1: + title: Extract Version Info from Android build.gradle + inputs: + - content: | + #!/bin/bash + set -e + + # Path to Android build.gradle file + BUILD_GRADLE_PATH="$PROJECT_LOCATION_ANDROID/app/build.gradle" + + # Extract versionName (remove quotes) + APP_SEM_VER_NAME_TMP=$(grep -o 'versionName "[^"]*"' "$BUILD_GRADLE_PATH" | sed 's/versionName "\(.*\)"/\1/') + + # Extract versionCode + APP_BUILD_NUMBER_TMP=$(grep -o 'versionCode [0-9]*' "$BUILD_GRADLE_PATH" | sed 's/versionCode \([0-9]*\)/\1/') + + # Validate that we found both values + if [ -z "$APP_SEM_VER_NAME_TMP" ] || [ -z "$APP_BUILD_NUMBER_TMP" ]; then + echo "Error: Could not extract version information from $BUILD_GRADLE_PATH" + echo "APP_SEM_VER_NAME: $APP_SEM_VER_NAME_TMP" + echo "APP_BUILD_NUMBER: $APP_SEM_VER_NAME_TMP" + exit 1 + fi + + echo "APP_SEM_VER_NAME: $APP_SEM_VER_NAME_TMP" + echo "APP_BUILD_NUMBER: $APP_BUILD_NUMBER_TMP" + + # Export as environment variables + envman add --key APP_SEM_VER_NAME --value "$APP_SEM_VER_NAME_TMP" + envman add --key APP_BUILD_NUMBER --value "$APP_BUILD_NUMBER_TMP" + install_applesimutils: + steps: + - script@1: + title: applesimutils installation + inputs: + - content: |- + #!/usr/bin/env bash + echo "Now installing applesimutils..." + brew tap wix/brew + brew install applesimutils + + # Notifications utility workflows + # Provides values for commit or branch message and path depending on commit env setup initialised or not + _get_workflow_info: + steps: + - activate-ssh-key@4: + is_always_run: true # always run to also feed failure notifications + run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' + - git-clone@6: + inputs: + - update_submodules: 'no' + is_always_run: true # always run to also feed failure notifications + - script@1: + is_always_run: true # always run to also feed failure notifications + inputs: + - content: | + #!/bin/bash + # generate reference to commit from env or using git + COMMIT_SHORT_HASH="${BITRISE_GIT_COMMIT:0:7}" + BRANCH_HEIGHT='' + WORKFLOW_TRIGGER='Push' + + if [[ -z "$BITRISE_GIT_COMMIT" ]]; then + COMMIT_SHORT_HASH="$(git rev-parse --short HEAD)" + BRANCH_HEIGHT='HEAD' + WORKFLOW_TRIGGER='Manual' + fi + + envman add --key COMMIT_SHORT_HASH --value "$COMMIT_SHORT_HASH" + envman add --key BRANCH_HEIGHT --value "$BRANCH_HEIGHT" + envman add --key WORKFLOW_TRIGGER --value "$WORKFLOW_TRIGGER" + title: Get commit or branch name and path variables + + # Slack notification utils: we have two workflows to allow choosing when to notify: on success, on failure or both. + # A workflow for instance create_qa_builds will notify on failure for each build_android_qa or build_ios_qa + # but will only notify success if both success and create_qa_builds succeeds. + + # Send a Slack message on successful release + release_announcing_stores: + before_run: + - code_setup + steps: + - yarn@0: + inputs: + - command: build:announce + title: Announcing pre-release + is_always_run: false + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: standard + + # Send a Slack message when workflow succeeds + notify_success: + before_run: + - _get_workflow_info + steps: + # Update Bitrise comment in PR with success status + - comment-on-github-pull-request@0: + is_always_run: true + run_if: '{{getenv "TRIGGERED_BY_PR_LABEL" | eq "true"}}' + inputs: + - personal_access_token: '$GITHUB_ACCESS_TOKEN' + - body: |- + ## [https://bitrise.io/](${BITRISEIO_PIPELINE_BUILD_URL}) **Bitrise** + + ✅✅✅ `${BITRISEIO_PIPELINE_TITLE}` passed on Bitrise! ✅✅✅ + + Commit hash: ${GITHUB_PR_HASH} + Build link: ${BITRISEIO_PIPELINE_BUILD_URL} + + >[!NOTE] + >- You can kick off another `${BITRISEIO_PIPELINE_TITLE}` on Bitrise by removing and re-applying the `run-ios-e2e-smoke` label on the pull request + + + + - repository_url: '$GIT_REPOSITORY_URL' + - issue_number: '$GITHUB_PR_NUMBER' + - api_base_url: 'https://api.github.com' + - update_comment_tag: '$GITHUB_PR_HASH' + - script@1: + is_always_run: true + title: Label PR with success + inputs: + - content: |- + #!/usr/bin/env bash + # Define label data + LABELS_JSON='{"labels":["bitrise-result-ready"]}' + + # API URL to add labels to a PR + API_URL="https://api.github.com/repos/$BITRISEIO_GIT_REPOSITORY_OWNER/$BITRISEIO_GIT_REPOSITORY_SLUG/issues/$GITHUB_PR_NUMBER/labels" + + # Perform the curl request and capture the HTTP status code + HTTP_RESPONSE=$(curl -s -o response.txt -w "%{http_code}" -X POST -H "Authorization: token $GITHUB_ACCESS_TOKEN" -H "Accept: application/vnd.github.v3+json" -d "$LABELS_JSON" "$API_URL") + + # Output the HTTP status code + echo "HTTP Response Code: $HTTP_RESPONSE" + + # Optionally check the response + echo "HTTP Response Code: $HTTP_RESPONSE" + + if [ "$HTTP_RESPONSE" -ne 200 ]; then + echo "Failed to apply label. Status code: $HTTP_RESPONSE" + cat response.txt # Show error message from GitHub if any + else + echo "Label applied successfully." + fi + + # Clean up the response file + rm response.txt + + + # Send a Slack message when workflow fails + notify_failure: + before_run: + - _get_workflow_info + steps: + - script@1: + is_always_run: true + title: Check if PR comment should be updated + inputs: + - content: |- + #!/usr/bin/env bash + if [[ "$TRIGGERED_BY_PR_LABEL" == "true" && $BITRISE_BUILD_STATUS == 1 ]]; then + envman add --key SHOULD_UPDATE_PR_COMMENT --value "true" + else + envman add --key SHOULD_UPDATE_PR_COMMENT --value "false" + fi + # Update Bitrise comment in PR with failure status + - comment-on-github-pull-request@0: + is_always_run: true + run_if: '{{getenv "SHOULD_UPDATE_PR_COMMENT" | eq "true"}}' + inputs: + - personal_access_token: '$GITHUB_ACCESS_TOKEN' + - body: |- + ## [https://bitrise.io/](${BITRISEIO_PIPELINE_BUILD_URL}) **Bitrise** + + ❌❌❌ `${BITRISEIO_PIPELINE_TITLE}` failed on Bitrise! ❌❌❌ + + Commit hash: ${GITHUB_PR_HASH} + Build link: ${BITRISEIO_PIPELINE_BUILD_URL} + + >[!NOTE] + >- You can rerun any failed steps by opening the Bitrise build, tapping `Rebuild` on the upper right then `Rebuild unsuccessful Workflows` + >- You can kick off another `${BITRISEIO_PIPELINE_TITLE}` on Bitrise by removing and re-applying the `run-ios-e2e-smoke` label on the pull request + + > [!TIP] + >- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise + + + + - repository_url: '$GIT_REPOSITORY_URL' + - issue_number: '$GITHUB_PR_NUMBER' + - api_base_url: 'https://api.github.com' + - update_comment_tag: '$GITHUB_PR_HASH' + bump_version_code: + before_run: + - _get_workflow_info + steps: + - script@1: + is_always_run: true + title: Trigger Update Build Version Action + inputs: + - content: |- + #!/usr/bin/env bash + set -e + + # Trigger the workflow + RESPONSE=$(curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TRIGGER_ACTION_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/MetaMask/metamask-mobile/actions/workflows/125632963/dispatches" \ + -d "{\"ref\":\"main\",\"inputs\":{\"base-branch\":\"$BITRISE_GIT_BRANCH\"}}" || exit 1) + + echo "Waiting 25 seconds for workflow to start..." + sleep 25 + + # Check completion status every 20 seconds + for i in {1..5}; do + echo "Checking workflow status (Attempt $i of 5)..." + + RESPONSE=$(curl -L \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer $GITHUB_TRIGGER_ACTION_TOKEN" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/MetaMask/metamask-mobile/actions/workflows/125632963/runs?branch=main&status=in_progress") + + # Store the total_count value + TOTAL_COUNT=$(echo "$RESPONSE" | jq -r '.total_count') + + if [ "$TOTAL_COUNT" = "0" ]; then + echo "Workflow finished result: https://github.com/MetaMask/metamask-mobile/actions/workflows/update-latest-build-version.yml" + exit 0 + else + # Get the status and conclusion of the most recent run + STATUS=$(echo "$RESPONSE" | jq -r '.workflow_runs[0].status') + echo "Current status: $STATUS" + echo "Workflow is still in progress (status: $STATUS)..." + sleep 20 + fi + done + + echo "Timeout: Workflow did not complete within 100 seconds" + echo "Check this action status for reason: https://github.com/MetaMask/metamask-mobile/actions/workflows/update-latest-build-version.yml" + exit 1 + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: standard + # CI Steps + ci_test: + before_run: + - code_setup + steps: + - yarn@0: + inputs: + - args: '' + - command: test:unit --silent + title: Unit Test + is_always_run: false + - script@1: + inputs: + - content: |- + #!/usr/bin/env bash + echo 'weew - everything passed!' + title: All Tests Passed + is_always_run: false + # E2E Steps + ### This workflow uses a flag (TEST_SUITE) that defines the specific set of tests to be run. + ## in this instance Regression. In future iterations we can rename to ios_test_suite_selection & android_test_suite_selection + ios_build_regression_tests: + after_run: + - ios_e2e_build + ios_run_regression_confirmations_tests: + envs: + - TEST_SUITE_TAG: 'RegressionConfirmations' + after_run: + - ios_e2e_test + ios_run_regression_wallet_platform_tests: + envs: + - TEST_SUITE_TAG: 'RegressionWalletPlatform' + after_run: + - ios_e2e_test + ios_run_regression_trade_tests: + envs: + - TEST_SUITE_TAG: 'RegressionTrade' + after_run: + - ios_e2e_test + ios_run_regression_network_abstraction_tests: + envs: + - TEST_SUITE_TAG: 'RegressionNetworkAbstractions' + after_run: + - ios_e2e_test + ios_run_regression_network_abstraction_tests_gns_disabled: + envs: + - TEST_SUITE_TAG: 'RegressionNetworkAbstractions' + after_run: + - ios_e2e_test + ios_run_regression_network_expansion_tests: + envs: + - TEST_SUITE_TAG: 'RegressionNetworkExpansion' + after_run: + - ios_e2e_test + ios_run_regression_performance_tests: + envs: + - TEST_SUITE_TAG: 'RegressionPerformance' + after_run: + - ios_e2e_test + ios_run_regression_accounts_tests: + envs: + - TEST_SUITE_TAG: 'RegressionAccounts' + after_run: + - ios_e2e_test + ios_run_regression_assets_tests: + envs: + - TEST_SUITE_TAG: 'RegressionAssets' + after_run: + - ios_e2e_test + ios_run_regression_ux_tests: + envs: + - TEST_SUITE_TAG: 'RegressionWalletUX' + after_run: + - ios_e2e_test + android_build_regression_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + after_run: + - android_e2e_build + android_run_regression_confirmations_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionConfirmations' + after_run: + - android_e2e_test + android_run_regression_wallet_platform_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionWalletPlatform' + after_run: + - android_e2e_test + android_run_regression_trade_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionTrade' + after_run: + - android_e2e_test + android_run_regression_network_abstraction_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionNetworkAbstractions' + after_run: + - android_e2e_test + android_run_regression_network_expansion_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionNetworkExpansion' + after_run: + - android_e2e_test + android_run_regression_performance_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionPerformance' + after_run: + - android_e2e_test + android_run_regression_accounts_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionAccounts' + after_run: + - android_e2e_test + android_run_regression_ux_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionWalletUX' + after_run: + - android_e2e_test + android_run_regression_assets_tests: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'RegressionAssets' + after_run: + - android_e2e_test + download_production_qa_apk: + steps: + - script@1: + title: Download Production QA APK + inputs: + - content: | + #!/usr/bin/env bash + ./tests/scripts/download-android-qa-app.sh + # APK_PATH is already set by the download script using envman + build_flask_e2e_android: + after_run: + - android_flask_e2e_build + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow since it's not used anymore + run_flask_e2e_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - METAMASK_BUILD_TYPE: 'flask' + after_run: + - android_e2e_test + build_flask_e2e_ios: + envs: + - COMMAND_YARN: 'build:ios:flask:e2e' + after_run: + - ios_e2e_build + # TODO: Remove this workflow since it's not used anymore + run_flask_e2e_ios: + envs: + - METAMASK_BUILD_TYPE: 'flask' + after_run: + - ios_e2e_test + build_ios_multichain_permissions_e2e: + after_run: + - ios_e2e_build + # - android_e2e_build + build_android_multichain_permissions_e2e: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + after_run: + - android_e2e_build + + ### Separating workflows so they run concurrently during smoke runs + run_tag_smoke_multichain_api_ios: + envs: + - TEST_SUITE_TAG: '.*SmokeMultiChainAPI.*' + after_run: + - ios_e2e_test + + run_network_expansion_swimlane_ios_smoke: + envs: + - TEST_SUITE_TAG: '.*NetworkExpansion.*' + after_run: + - ios_e2e_test + run_network_expansion_swimlane_android_smoke: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*NetworkExpansion.*' + after_run: + - android_e2e_test + + run_wallet_platform_swimlane_ios_smoke: + envs: + - TEST_SUITE_TAG: '.*SmokeWalletPlatform.*' + after_run: + - ios_e2e_test + run_wallet_platform_swimlane_android_smoke: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokeWalletPlatform.*' + after_run: + - android_e2e_test + + run_network_abstraction_swimlane_ios_smoke: + envs: + - TEST_SUITE_TAG: '.*SmokeNetworkAbstraction.*' + after_run: + - ios_e2e_test + run_network_abstraction_swimlane_android_smoke: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokeNetworkAbstraction.*' + after_run: + - android_e2e_test + run_trade_swimlane_ios_smoke: + envs: + - TEST_SUITE_TAG: '.*(SmokeSwap|SmokeStake).*' + after_run: + - ios_e2e_test + run_trade_swimlane_android_smoke: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*(SmokeSwap|SmokeStake).*' + after_run: + - android_e2e_test + run_ios_api_specs: + after_run: + - ios_api_specs + run_tag_smoke_confirmations_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'SmokeConfirmations' + after_run: + - android_e2e_test + run_tag_smoke_confirmations_ios: + envs: + - TEST_SUITE_TAG: 'SmokeConfirmations' + after_run: + - ios_e2e_test + run_tag_smoke_performance_ios: + envs: + - TEST_SUITE_TAG: '.*SmokePerformance.*' + after_run: + - ios_e2e_test + run_tag_smoke_performance_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokePerformance.*' + after_run: + - android_e2e_test + run_tag_multichain_permissions_ios: + envs: + - TEST_SUITE_TAG: '.*SmokeMultiChainPermissions.*' + after_run: + - ios_e2e_test + run_tag_multichain_permissions_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokeMultiChainPermissions.*' + after_run: + - android_e2e_test + run_tag_flask_build_tests_ios: + envs: + - TEST_SUITE_TAG: '.*FlaskBuildTests.*' + after_run: + - ios_e2e_test + run_tag_flask_build_tests_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*FlaskBuildTests.*' + after_run: + - android_e2e_test + run_tag_smoke_identity_ios: + envs: + - TEST_SUITE_TAG: 'SmokeIdentity' + after_run: + - ios_e2e_test + run_tag_smoke_identity_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'SmokeIdentity' + after_run: + - android_e2e_test + run_tag_smoke_accounts_ios: + envs: + - TEST_SUITE_TAG: 'SmokeAccounts' + after_run: + - ios_e2e_test + run_tag_smoke_accounts_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: 'SmokeAccounts' + after_run: + - android_e2e_test + run_tag_smoke_money_ios: + envs: + - TEST_SUITE_TAG: '.*SmokeMoney.*' + after_run: + - ios_e2e_test + run_tag_smoke_money_android: + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + envs: + - TEST_SUITE_TAG: '.*SmokeMoney.*' + after_run: + - android_e2e_test + android_e2e_build: + envs: + - KEYSTORE_URL: $BITRISEIO_ANDROID_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/release.keystore' + - BUILD_COMMAND: 'yarn build:android:main:e2e' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + # TODO: Consolidate android_e2e_build and android_flask_e2e_build once _android_e2e_build_template and _android_build_template is consolidated + android_flask_e2e_build: + envs: + - KEYSTORE_URL: $BITRISEIO_ANDROID_FLASK_KEYSTORE_URL_URL + - KEYSTORE_PATH: 'android/keystores/flaskRelease.keystore' + - BUILD_COMMAND: 'yarn build:android:flask:e2e' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + run_single_android_e2e_test: + run_if: '{{ or (ne "$E2E_TEST_FILE" "") (ne "$TEST_SUITE_TAG" "") }}' + after_run: + - android_e2e_test + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + run_single_ios_e2e_test: + run_if: '{{ or (ne "$E2E_TEST_FILE" "") (ne "$TEST_SUITE_TAG" "") }}' + after_run: + - ios_e2e_test + + android_e2e_test: + before_run: + - setup + - prep_environment + after_run: + - notify_failure + steps: + - restore-gradle-cache@2: {} + - restore-cache@2: + title: Restore Android PR Build Cache (if build was skipped) + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "true"}}' + inputs: + - key: '{{ getenv "ANDROID_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Copy Android build from cache (if build was skipped) + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "true"}}' + inputs: + - content: |- + #!/usr/bin/env bash + echo "Copying Android build from cache..." + + if [ -d "/tmp/android-cache/build/outputs" ]; then + echo "Restoring Android build outputs from cache..." + mkdir -p android/app/build/outputs + cp -r /tmp/android-cache/build/outputs/* android/app/build/outputs/ + echo "✅ Android build artifacts restored from cache" + + echo "Restored files:" + find android/app/build/outputs -type f -name "*.apk" -o -name "*.aab" | head -5 + else + echo "❌ Cache directory /tmp/android-cache/build/outputs not found" + echo "Cache may not have been restored properly" + fi + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull Android build + - script@1: + title: Copy Android build for Detox + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/android/app/build/outputs" + + # Copy saved files for Detox usage + # INTERMEDIATE_ANDROID_BUILD_DIR is the cached directory from android_e2e_build's "Save Android build" step + cp -r "$INTERMEDIATE_ANDROID_BUILD_DIR" "$BITRISE_SOURCE_DIR/android/app/build" + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - avd-manager@1: + inputs: + - api_level: '34' + - abi: 'x86_64' + - create_command_flags: --sdcard 8192M + - start_command_flags: -read-only + - profile: pixel_5 + - wait-for-android-emulator@1: {} + - script@1: + title: Run detox test + timeout: 1800 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + export METAMASK_ENVIRONMENT='dev' + node -v + if [ -n "${E2E_TEST_FILE:-}" ]; then + echo "[INFO] Running only specified E2E_TEST_FILE(s): $E2E_TEST_FILE" + IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:android:run:qa-release $E2E_TEST_FILE + elif [ -n "${TEST_SUITE_TAG:-}" ]; then + echo "[INFO] Running tests matching TEST_SUITE_TAG: $TEST_SUITE_TAG" + ./tests/scripts/run-e2e-tags.sh + fi + - custom-test-results-export@1: + title: Export test results + is_always_run: true + is_skippable: true + inputs: + - base_path: $BITRISE_SOURCE_DIR/tests/reports/ + - test_name: E2E Tests + - search_pattern: $BITRISE_SOURCE_DIR/tests/reports/junit.xml + - deploy-to-bitrise-io@2.2.3: + title: Deploy test report files + is_always_run: true + is_skippable: true + - script@1: + title: Copy screenshot files + is_always_run: true + run_if: .IsBuildFailed + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" + - deploy-to-bitrise-io@2.3: + title: Deploy test screenshots + is_always_run: true + run_if: .IsBuildFailed + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR + - is_compress: true + - zip_name: E2E_Android_Failure_Artifacts + - script@1: + title: Copy performance results + is_always_run: true + run_if: '{{getenv "TEST_SUITE_TAG" | eq ".*SmokePerformance.*"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + # Create performance results directory + mkdir -p "$BITRISE_DEPLOY_DIR/performance-results" + + # Copy performance JSON files if they exist + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied account-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied network-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied switching-accounts-to-dismiss-load-testing-performance-results.json" + fi + - deploy-to-bitrise-io@2.3: + title: Deploy performance results + is_always_run: true + run_if: '{{getenv "TEST_SUITE_TAG" | eq ".*SmokePerformance.*"}}' + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR/performance-results + - is_compress: true + - zip_name: E2E_Performance_Results + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + # Performance-specific Android E2E test workflow + android_e2e_test_performance: + before_run: + - setup + - prep_environment + after_run: + - notify_failure + steps: + - restore-gradle-cache@2: {} + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull Android build + - script@1: + title: Copy Android build for Detox + run_if: '{{getenv "SKIP_ANDROID_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/android/app/build/outputs" + + # Copy saved files for Detox usage + # INTERMEDIATE_ANDROID_BUILD_DIR is the cached directory from android_e2e_build's "Save Android build" step + cp -r "$INTERMEDIATE_ANDROID_BUILD_DIR" "$BITRISE_SOURCE_DIR/android/app/build" + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - avd-manager@1: + inputs: + - api_level: '34' + - abi: 'x86_64' + - create_command_flags: --sdcard 8192M + - start_command_flags: -read-only + - profile: pixel_5 + - wait-for-android-emulator@1: {} + - script@1: + title: Run detox test + timeout: 1200 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + + export METAMASK_ENVIRONMENT='e2e' + + if [ -n "${E2E_TEST_FILE:-}" ]; then + echo "[INFO] Running only specified E2E_TEST_FILE(s): $E2E_TEST_FILE" + IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:android:$METAMASK_BUILD_TYPE:prod $E2E_TEST_FILE + elif [ -n "${TEST_SUITE_TAG:-}" ]; then + echo "[INFO] Running tests matching TEST_SUITE_TAG: $TEST_SUITE_TAG" + ./tests/scripts/run-e2e-tags.sh + fi + - custom-test-results-export@1: + title: Export test results + is_always_run: true + is_skippable: true + inputs: + - base_path: $BITRISE_SOURCE_DIR/tests/reports/ + - test_name: E2E Tests + - search_pattern: $BITRISE_SOURCE_DIR/tests/reports/junit.xml + - deploy-to-bitrise-io@2.2.3: + title: Deploy test report files + is_always_run: true + is_skippable: true + - script@1: + title: Copy screenshot files + is_always_run: true + run_if: .IsBuildFailed + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" + - deploy-to-bitrise-io@2.3: + title: Deploy test screenshots + is_always_run: true + run_if: .IsBuildFailed + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR + - is_compress: true + - zip_name: E2E_Android_Failure_Artifacts + - script@1: + title: Copy performance results + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + # Create performance results directory + mkdir -p "$BITRISE_DEPLOY_DIR/performance-results" + + # Copy performance JSON files if they exist + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied account-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied network-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied switching-accounts-to-dismiss-load-testing-performance-results.json" + fi + - deploy-to-bitrise-io@2.3: + title: Deploy performance results + is_always_run: true + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR/performance-results + - is_compress: true + - zip_name: E2E_Performance_Results + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + ios_api_specs: + before_run: + - setup + - code_setup + - install_applesimutils + - prep_environment + after_run: + - notify_failure + steps: + - restore-cache@2: + title: Restore iOS PR Build Cache (if build was skipped) + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "true"}}' + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Copy iOS build from cache (if build was skipped) + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "true"}}' + inputs: + - content: |- + #!/usr/bin/env bash + echo "Copying iOS build from cache..." + + # Check if cached build products exist + if [ -d "ios/build/Build/Products/Release-iphonesimulator" ]; then + echo "✅ iOS build artifacts found in cache" + echo "Build products directory contents:" + ls -la ios/build/Build/Products/Release-iphonesimulator/ | head -5 + else + echo "❌ iOS build products not found in cache" + mkdir -p ios/build/Build/Products/Release-iphonesimulator + fi + + # Check if cached Detox artifacts exist + if [ -d "../Library/Detox/ios" ]; then + echo "✅ Detox iOS artifacts found in cache" + echo "Detox directory contents:" + ls -la ../Library/Detox/ios/ | head -5 + else + echo "❌ Detox iOS artifacts not found in cache" + mkdir -p ../Library/Detox/ios + fi + + echo "iOS build artifacts restored from cache" + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull iOS build + - script@1: + title: Copy iOS build for Detox + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + mkdir -p "$BITRISE_SOURCE_DIR/../Library/Detox/ios" + + # Copy saved files for Detox usage + # INTERMEDIATE_IOS_BUILD_DIR & INTERMEDIATE_IOS_DETOX_DIR are the cached directories by ios_e2e_build's "Save iOS build" step + cp -r "$INTERMEDIATE_IOS_BUILD_DIR" "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + cp -r "$INTERMEDIATE_IOS_DETOX_DIR" "$BITRISE_SOURCE_DIR/../Library/Detox" + # - restore-cocoapods-cache@2: {} + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - certificate-and-profile-installer@1: {} + - set-xcode-build-number@1: + inputs: + - build_short_version_string: $VERSION_NAME + - plist_path: $PROJECT_LOCATION_IOS/MetaMask/Info.plist + - script: + inputs: + - content: |- + # Add cache directory to environment variable + envman add --key BREW_APPLESIMUTILS --value "$(brew --cellar)/applesimutils" + envman add --key BREW_OPT_APPLESIMUTILS --value "/usr/local/opt/applesimutils" + brew tap wix/brew + title: Set Env Path for caching deps + - script@1: + title: Run detox test + timeout: 1800 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + yarn test:api-specs --retries 1 + - script@1: + is_always_run: true + is_skippable: false + title: Add tests reports to Bitrise + inputs: + - content: |- + #!/usr/bin/env bash + cp -r $BITRISE_SOURCE_DIR/html-report/index.html $BITRISE_HTML_REPORT_DIR/ + - deploy-to-bitrise-io@2.2.3: + is_always_run: true + is_skippable: false + inputs: + - deploy_path: $BITRISE_HTML_REPORT_DIR + title: Deploy test report files + + pr_check_build_cache: + steps: + - activate-ssh-key@4: + run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' + - git-clone@6: {} + - restore-cache@2: + title: Restore last successful build commit marker + is_skippable: true + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - script@1: + title: Generate cache keys and check both iOS and Android builds + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/generate-pr-cache-keys.sh + - restore-cache@2: + title: Check iOS cache + is_skippable: true + run_if: '{{getenv "IOS_PR_BUILD_CACHE_KEY" | ne ""}}' + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Process iOS cache result + run_if: '{{getenv "IOS_PR_BUILD_CACHE_KEY" | ne ""}}' + inputs: + - content: |- + #!/usr/bin/env bash + if [[ "$BITRISE_CACHE_HIT" == "exact" ]]; then + echo "✅ iOS cache found - build will be skipped" + envman add --key SKIP_IOS_BUILD --value "true" + else + echo "❌ iOS cache not found - build will proceed" + envman add --key SKIP_IOS_BUILD --value "false" + fi + envman add --key BITRISE_CACHE_HIT --value "" # Reset for next check + - restore-cache@2: + title: Check Android cache + is_skippable: true + run_if: '{{getenv "ANDROID_PR_BUILD_CACHE_KEY" | ne ""}}' + inputs: + - key: '{{ getenv "ANDROID_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Process Android cache result and finalize decisions + inputs: + - content: |- + #!/usr/bin/env bash + + # Initialize local variables with current state + LOCAL_SKIP_IOS="$SKIP_IOS_BUILD" + LOCAL_SKIP_ANDROID="$SKIP_ANDROID_BUILD" + + # Ensure iOS value is set if cache check was skipped + if [[ -z "$IOS_PR_BUILD_CACHE_KEY" ]]; then + echo "No iOS cache key available - build will proceed" + LOCAL_SKIP_IOS="false" + envman add --key SKIP_IOS_BUILD --value "false" + fi + + # Only process Android cache if we have cache keys + if [[ -n "$ANDROID_PR_BUILD_CACHE_KEY" ]]; then + if [[ "$BITRISE_CACHE_HIT" == "exact" ]]; then + echo "✅ Android cache found - build will be skipped" + LOCAL_SKIP_ANDROID="true" + envman add --key SKIP_ANDROID_BUILD --value "true" + else + echo "❌ Android cache not found - build will proceed" + LOCAL_SKIP_ANDROID="false" + envman add --key SKIP_ANDROID_BUILD --value "false" + fi + else + echo "No Android cache key available - build will proceed" + LOCAL_SKIP_ANDROID="false" + envman add --key SKIP_ANDROID_BUILD --value "false" + fi + + echo "" + echo "=== Final Build Decisions ===" + echo "iOS build: $([ "$LOCAL_SKIP_IOS" == "true" ] && echo "SKIP" || echo "BUILD")" + echo "Android build: $([ "$LOCAL_SKIP_ANDROID" == "true" ] && echo "SKIP" || echo "BUILD")" + - share-pipeline-variable@1: + title: Share variables across pipeline stages + inputs: + - variables: |- + SKIP_IOS_BUILD + SKIP_ANDROID_BUILD + IOS_PR_BUILD_CACHE_KEY + ANDROID_PR_BUILD_CACHE_KEY + + ios_e2e_build: + before_run: + - install_applesimutils + - code_setup + - set_commit_hash + after_run: + - notify_failure + steps: + - script@1: + title: Generating ccache key using native folder checksum + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/cache/set-cache-envs.sh ios + - certificate-and-profile-installer@1: {} + - script: + inputs: + - content: |- + # Add cache directory to environment variable + envman add --key BREW_APPLESIMUTILS --value "$(brew --cellar)/applesimutils" + envman add --key BREW_OPT_APPLESIMUTILS --value "/usr/local/opt/applesimutils" + brew tap wix/brew + title: Set Env Path for caching deps + - script@1: + title: Install CCache & symlink + inputs: + - content: |- + #!/usr/bin/env bash + brew install ccache with HOMEBREW_NO_DEPENDENTS_CHECK=1 + ln -s $(which ccache) /usr/local/bin/gcc + ln -s $(which ccache) /usr/local/bin/g++ + ln -s $(which ccache) /usr/local/bin/cc + ln -s $(which ccache) /usr/local/bin/c++ + ln -s $(which ccache) /usr/local/bin/clang + ln -s $(which ccache) /usr/local/bin/clang++ + - restore-cache@2: + title: Restore CCache + inputs: + - key: '{{ getenv "CCACHE_KEY" }}' + - script@1: + title: Set skip ccache upload + run_if: '{{ enveq "BITRISE_CACHE_HIT" "exact" }}' + inputs: + - 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 + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/cache/setup-ccache.sh + node -v + if [ -n "$COMMAND_YARN" ]; then + GIT_BRANCH=$BITRISE_GIT_BRANCH yarn "$COMMAND_YARN" + else + echo "No COMMAND_YARN provided. Running yarn build:ios:main:e2e..." + yarn build:ios:main:e2e + fi + - save-cocoapods-cache@1: {} + - save-cache@1: + title: Save CCache + run_if: '{{not (enveq "SKIP_CCACHE_UPLOAD" "true")}}' + inputs: + - key: '{{ getenv "CCACHE_KEY" }}' + - paths: |- + ccache + - save-cache@1: + title: Save iOS PR Build Cache + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - paths: |- + ios/build/Build/Products/Release-iphonesimulator + ../Library/Detox/ios + - script@1: + title: Save last successful build commit + run_if: '{{getenv "GITHUB_PR_NUMBER" | ne ""}}' + inputs: + - content: |- + #!/usr/bin/env bash + # Create a marker file with the current commit + mkdir -p /tmp/last-build-commit + echo "$(git rev-parse HEAD 2>/dev/null || echo ${BITRISE_GIT_COMMIT})" > /tmp/last-build-commit/commit + echo "Build completed successfully at $(date)" >> /tmp/last-build-commit/commit + - save-cache@1: + title: Save last successful build commit marker + run_if: '{{getenv "GITHUB_PR_NUMBER" | ne ""}}' + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - paths: /tmp/last-build-commit + - deploy-to-bitrise-io@2.2.3: + inputs: + - pipeline_intermediate_files: |- + ios/build/Build/Products/Release-iphonesimulator:INTERMEDIATE_IOS_BUILD_DIR + ../Library/Detox/ios:INTERMEDIATE_IOS_DETOX_DIR + title: Save iOS build + - save-cache@1: + title: Save node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - paths: node_modules + ios_e2e_test: + before_run: + - setup + - code_setup + - install_applesimutils + - prep_environment + after_run: + - notify_failure + steps: + - restore-cache@2: + title: Restore iOS PR Build Cache (if build was skipped) + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "true"}}' + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - script@1: + title: Copy iOS build from cache (if build was skipped) + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "true"}}' + inputs: + - content: |- + #!/usr/bin/env bash + echo "Copying iOS build from cache..." + + # Check if cached build products exist + if [ -d "ios/build/Build/Products/Release-iphonesimulator" ]; then + echo "✅ iOS build artifacts found in cache" + echo "Build products directory contents:" + ls -la ios/build/Build/Products/Release-iphonesimulator/ | head -5 + else + echo "❌ iOS build products not found in cache" + mkdir -p ios/build/Build/Products/Release-iphonesimulator + fi + + # Check if cached Detox artifacts exist + if [ -d "../Library/Detox/ios" ]; then + echo "✅ Detox iOS artifacts found in cache" + echo "Detox directory contents:" + ls -la ../Library/Detox/ios/ | head -5 + else + echo "❌ Detox iOS artifacts not found in cache" + mkdir -p ../Library/Detox/ios + fi + + echo "iOS build artifacts restored from cache" + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull iOS build + - script@1: + title: Copy iOS build for Detox + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + mkdir -p "$BITRISE_SOURCE_DIR/../Library/Detox/ios" + + # Copy saved files for Detox usage + # INTERMEDIATE_IOS_BUILD_DIR & INTERMEDIATE_IOS_DETOX_DIR are the cached directories by ios_e2e_build's "Save iOS build" step + cp -r "$INTERMEDIATE_IOS_BUILD_DIR" "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + cp -r "$INTERMEDIATE_IOS_DETOX_DIR" "$BITRISE_SOURCE_DIR/../Library/Detox" + # - restore-cocoapods-cache@2: {} + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - certificate-and-profile-installer@1: {} + - set-xcode-build-number@1: + inputs: + - build_short_version_string: $VERSION_NAME + - plist_path: $PROJECT_LOCATION_IOS/MetaMask/Info.plist + - script: + inputs: + - content: |- + # Add cache directory to environment variable + envman add --key BREW_APPLESIMUTILS --value "$(brew --cellar)/applesimutils" + envman add --key BREW_OPT_APPLESIMUTILS --value "/usr/local/opt/applesimutils" + brew tap wix/brew + title: Set Env Path for caching deps + - script@1: + title: Boot up simulator + inputs: + - content: |- + #!/usr/bin/env bash + xcrun simctl boot "iPhone 15 Pro" || true + xcrun simctl list | grep Booted + - script@1: + title: Run detox test + timeout: 1800 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + + # node -v + export METAMASK_ENVIRONMENT='dev' + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + # if [ "$METAMASK_BUILD_TYPE" = "flask" ]; then + # IS_TEST='true' METAMASK_BUILD_TYPE='flask' yarn test:e2e:ios:run:qa-release e2e/specs/flask/ + # else + # ./tests/scripts/run-e2e-tags.sh + # fi + if [ -n "${E2E_TEST_FILE:-}" ]; then + echo "[INFO] Running only specified E2E_TEST_FILE(s): $E2E_TEST_FILE" + IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:ios:run:qa-release $E2E_TEST_FILE + elif [ -n "${TEST_SUITE_TAG:-}" ]; then + echo "[INFO] Running tests matching TEST_SUITE_TAG: $TEST_SUITE_TAG" + ./tests/scripts/run-e2e-tags.sh + fi + - custom-test-results-export@1: + is_always_run: true + is_skippable: false + title: Export test results + inputs: + - base_path: $BITRISE_SOURCE_DIR/tests/reports/ + - test_name: E2E Tests + - search_pattern: $BITRISE_SOURCE_DIR/tests/reports/junit.xml + - deploy-to-bitrise-io@2.2.3: + is_always_run: true + is_skippable: true + title: Deploy test report files + - script@1: + is_always_run: true + run_if: .IsBuildFailed + title: Copy screenshot files + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" + - deploy-to-bitrise-io@2.3: + is_always_run: true + run_if: .IsBuildFailed + title: Deploy test screenshots + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR + - is_compress: true + - zip_name: 'E2E_IOS_Failure_Artifacts' + - script@1: + title: Copy performance results + is_always_run: true + run_if: '{{getenv "TEST_SUITE_TAG" | eq ".*SmokePerformance.*"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + # Create performance results directory + mkdir -p "$BITRISE_DEPLOY_DIR/performance-results" + + # Copy performance JSON files if they exist + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied account-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied network-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied switching-accounts-to-dismiss-load-testing-performance-results.json" + fi + - deploy-to-bitrise-io@2.3: + title: Deploy performance results + is_always_run: true + run_if: '{{getenv "TEST_SUITE_TAG" | eq ".*SmokePerformance.*"}}' + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR/performance-results + - is_compress: true + - zip_name: E2E_Performance_Results + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + # Performance-specific iOS E2E test workflow + ios_e2e_test_performance: + before_run: + - setup + - install_applesimutils + - prep_environment + after_run: + - notify_failure + steps: + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull iOS build + - script@1: + title: Copy iOS build for Detox + run_if: '{{getenv "SKIP_IOS_BUILD" | eq "false"}}' + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Create directories for Detox + mkdir -p "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + mkdir -p "$BITRISE_SOURCE_DIR/../Library/Detox/ios" + + # Copy saved files for Detox usage + # INTERMEDIATE_IOS_BUILD_DIR & INTERMEDIATE_IOS_DETOX_DIR are the cached directories by ios_e2e_build's "Save iOS build" step + cp -r "$INTERMEDIATE_IOS_BUILD_DIR" "$BITRISE_SOURCE_DIR/ios/build/Build/Products" + cp -r "$INTERMEDIATE_IOS_DETOX_DIR" "$BITRISE_SOURCE_DIR/../Library/Detox" + # - restore-cocoapods-cache@2: {} + - restore-cache@2: + title: Restore cache node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - script@1: + title: Install foundry + inputs: + - content: |- + #!/bin/bash + yarn install:foundryup + - certificate-and-profile-installer@1: {} + - set-xcode-build-number@1: + inputs: + - build_short_version_string: $VERSION_NAME + - plist_path: $PROJECT_LOCATION_IOS/MetaMask/MetaMask-QA-Info.plist + - script: + inputs: + - content: |- + # Add cache directory to environment variable + envman add --key BREW_APPLESIMUTILS --value "$(brew --cellar)/applesimutils" + envman add --key BREW_OPT_APPLESIMUTILS --value "/usr/local/opt/applesimutils" + brew tap wix/brew + title: Set Env Path for caching deps + - script@1: + title: Boot up simulator + inputs: + - content: |- + #!/usr/bin/env bash + xcrun simctl boot "iPhone 15 Pro" || true + xcrun simctl list | grep Booted + - script@1: + title: Run detox test + timeout: 1200 + is_always_run: false + inputs: + - content: |- + #!/usr/bin/env bash + + export METAMASK_ENVIRONMENT='e2e' + + if [ -n "${E2E_TEST_FILE:-}" ]; then + echo "[INFO] Running only specified E2E_TEST_FILE(s): $E2E_TEST_FILE" + IGNORE_BOXLOGS_DEVELOPMENT="true" yarn test:e2e:ios:$METAMASK_BUILD_TYPE:prod $E2E_TEST_FILE + elif [ -n "${TEST_SUITE_TAG:-}" ]; then + echo "[INFO] Running tests matching TEST_SUITE_TAG: $TEST_SUITE_TAG" + ./tests/scripts/run-e2e-tags.sh + fi + - custom-test-results-export@1: + is_always_run: true + is_skippable: false + title: Export test results + inputs: + - base_path: $BITRISE_SOURCE_DIR/tests/reports/ + - test_name: E2E Tests + - search_pattern: $BITRISE_SOURCE_DIR/tests/reports/junit.xml + - deploy-to-bitrise-io@2.2.3: + is_always_run: true + is_skippable: true + title: Deploy test report files + - script@1: + is_always_run: true + run_if: .IsBuildFailed + title: Copy screenshot files + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + cp -r "$BITRISE_SOURCE_DIR/artifacts" "$BITRISE_DEPLOY_DIR" + - deploy-to-bitrise-io@2.3: + is_always_run: true + run_if: .IsBuildFailed + title: Deploy test screenshots + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR + - is_compress: true + - zip_name: 'E2E_IOS_Failure_Artifacts' + - script@1: + title: Copy performance results + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + # Create performance results directory + mkdir -p "$BITRISE_DEPLOY_DIR/performance-results" + + # Copy performance JSON files if they exist + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/account-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied account-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/network-list-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied network-list-load-testing-performance-results.json" + fi + + if [ -f "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" ]; then + cp "$BITRISE_SOURCE_DIR/tests/smoke/performance/reports/switching-accounts-to-dismiss-load-testing-performance-results.json" "$BITRISE_DEPLOY_DIR/performance-results/" + echo "Copied switching-accounts-to-dismiss-load-testing-performance-results.json" + fi + - deploy-to-bitrise-io@2.3: + title: Deploy performance results + is_always_run: true + inputs: + - deploy_path: $BITRISE_DEPLOY_DIR/performance-results + - is_compress: true + - zip_name: E2E_Performance_Results + meta: + bitrise.io: + machine_type_id: elite-xl + stack: linux-docker-android-22.04 + + start_e2e_tests: + steps: + - build-router-start@0: + inputs: + - workflows: |- + ios_e2e_test + - wait_for_builds: 'true' + - access_token: $BITRISE_START_BUILD_ACCESS_TOKEN + - build-router-wait@0: + inputs: + - abort_on_fail: 'yes' + - access_token: $BITRISE_START_BUILD_ACCESS_TOKEN + # Runway Workflow for Release Candidate Builds + runway_build_release_candidate: + before_run: + - bump_version_code + after_run: + - build_ios_release_and_upload_sourcemaps + - build_android_release_and_upload_sourcemaps + # Android Builds + _android_build_template: + before_run: + - code_setup + - extract_version_info + after_run: + - notify_failure + steps: + - file-downloader@1: + inputs: + - source: $KEYSTORE_FILE_PATH + - destination: $KEYSTORE_PATH + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + # TapAndPay SDK Setup + - script@1: + title: Clone TapAndPay SDK + inputs: + - content: |- + #!/usr/bin/env bash + set -euo pipefail + mkdir -p ~/.ssh + echo "$TAP_AND_PAY_SDK_SSH_KEY" | base64 -d | tr -d '\r' > ~/.ssh/tap_and_pay_key + echo "" >> ~/.ssh/tap_and_pay_key + chmod 600 ~/.ssh/tap_and_pay_key + ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null + eval "$(ssh-agent -s)" + ssh-add ~/.ssh/tap_and_pay_key + echo "Cloning TapAndPay SDK into android/libs/..." + git clone --depth 1 git@github.com:MetaMask/tap-and-pay-sdk.git /tmp/tap-and-pay-sdk + mkdir -p android/libs + cp -r /tmp/tap-and-pay-sdk/* android/libs/ + rm -rf /tmp/tap-and-pay-sdk + rm -f ~/.ssh/tap_and_pay_key + echo "TapAndPay SDK installed to android/libs/" + - restore-gradle-cache@2: {} + - install-missing-android-tools@3: + inputs: + - ndk_version: $NDK_VERSION + - gradlew_path: $PROJECT_LOCATION/gradlew + # Note - This step will fail if stack is not Linux + - script@1: + title: Install ICU libraries for Hermes + inputs: + - content: |- + #!/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 + inputs: + - content: |- + #!/usr/bin/env bash + node -v + if [ -n "$COMMAND_YARN" ]; then + GIT_BRANCH=$BITRISE_GIT_BRANCH yarn "$COMMAND_YARN" + else + echo "No COMMAND_YARN provided" + exit 1 + fi + - deploy-to-bitrise-io@2.2.3: + title: Share Detox files between pipelines + run_if: '{{getenv "SHARE_WITH_DETOX" | eq "true"}}' + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: android/app/build/outputs:INTERMEDIATE_ANDROID_BUILD_DIR + - save-gradle-cache@1: {} + - script@1: + title: Rename release files + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Set base paths for release builds + if [ "$IS_DEV_BUILD" = "true" ]; then + APK_DIR="$PROJECT_LOCATION/app/build/outputs/apk/$APP_NAME/debug" + else + APK_DIR="$PROJECT_LOCATION/app/build/outputs/apk/$APP_NAME/release" + BUNDLE_DIR="$PROJECT_LOCATION/app/build/outputs/bundle/$OUTPUT_PATH" + fi + + # Generate new names based on build type and version + if [ -n "$COMMAND_YARN" ]; then + NAME_FROM_YARN_COMMAND="$(cut -d':' -f3- <<< "$COMMAND_YARN" | sed 's/:/-/g')" + NEW_BASE_NAME="metamask-${NAME_FROM_YARN_COMMAND}-${APP_BUILD_NUMBER}" + else + NEW_BASE_NAME="metamask-${METAMASK_ENVIRONMENT}-${METAMASK_BUILD_TYPE}-${APP_SEM_VER_NAME}-${APP_BUILD_NUMBER}" + fi + + # Rename APK + if [ "$IS_DEV_BUILD" = "true" ]; then + OLD_APK="$APK_DIR/app-$APP_NAME-debug.apk" + OLD_AAB="$BUNDLE_DIR/app-$APP_NAME-debug.aab" + else + OLD_APK="$APK_DIR/app-$APP_NAME-release.apk" + OLD_AAB="$BUNDLE_DIR/app-$APP_NAME-release.aab" + fi + + NEW_APK="$APK_DIR/$NEW_BASE_NAME.apk" + cp "$OLD_APK" "$NEW_APK" + + # Rename AAB + if [ -n "$BUNDLE_DIR" ]; then + NEW_AAB="$BUNDLE_DIR/$NEW_BASE_NAME.aab" + cp "$OLD_AAB" "$NEW_AAB" + fi + + # Export new names as environment variables + envman add --key RENAMED_APK_FILE --value "$NEW_BASE_NAME.apk" + envman add --key RENAMED_AAB_FILE --value "$NEW_BASE_NAME.aab" + envman add --key APK_DEPLOY_PATH --value "$APK_DIR/$NEW_BASE_NAME.apk" + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - deploy_path: $APK_DEPLOY_PATH + title: Bitrise Deploy APK + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + inputs: + - pipeline_intermediate_files: $PROJECT_LOCATION/app/build/outputs/apk/$APP_NAME/release/sha512sums.txt:BITRISE_PLAY_STORE_SHA512SUMS_PATH + - deploy_path: $PROJECT_LOCATION/app/build/outputs/apk/$APP_NAME/release/sha512sums.txt + title: Bitrise Deploy Checksum + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + inputs: + - pipeline_intermediate_files: $PROJECT_LOCATION/app/build/outputs/mapping/$OUTPUT_PATH/mapping.txt:BITRISE_PLAY_STORE_MAPPING_PATH + - deploy_path: $PROJECT_LOCATION/app/build/outputs/mapping/$OUTPUT_PATH/mapping.txt + title: Bitrise ProGuard Map Files + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + inputs: + - pipeline_intermediate_files: $PROJECT_LOCATION/app/build/outputs/bundle/$OUTPUT_PATH/$RENAMED_AAB_FILE:BITRISE_PLAY_STORE_ABB_PATH + - deploy_path: $PROJECT_LOCATION/app/build/outputs/bundle/$OUTPUT_PATH/$RENAMED_AAB_FILE + title: Bitrise Deploy AAB + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + inputs: + - deploy_path: $PROJECT_LOCATION/app/build/generated/sourcemaps/react/$OUTPUT_PATH + - is_compress: true + - zip_name: Android_Sourcemaps_$OUTPUT_PATH + title: Deploy Android Sourcemaps + - script@1: + title: Prepare Android build outputs for caching + run_if: '{{and (getenv "ANDROID_PR_BUILD_CACHE_KEY" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - content: |- + #!/usr/bin/env bash + echo "=== Preparing Android cache ===" + echo "Current working directory: $(pwd)" + + # Create a clean cache directory structure + mkdir -p /tmp/android-cache/build/outputs + + if [ -d "android/app/build/outputs" ]; then + echo "Copying Android build outputs to cache staging area..." + cp -r android/app/build/outputs/* /tmp/android-cache/build/outputs/ + echo "Cache staging completed" + + echo "Cache contents:" + find /tmp/android-cache -type f | head -10 + echo "Total cache size: $(du -sh /tmp/android-cache 2>/dev/null || echo 'Unknown')" + else + echo "Warning: android/app/build/outputs not found!" + fi + - save-cache@1: + title: Save Android PR Build Cache + run_if: '{{and (getenv "ANDROID_PR_BUILD_CACHE_KEY" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - key: '{{ getenv "ANDROID_PR_BUILD_CACHE_KEY" }}' + - paths: |- + /tmp/android-cache + - script@1: + title: Save last successful build commit (Android) + run_if: '{{and (getenv "GITHUB_PR_NUMBER" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - content: |- + #!/usr/bin/env bash + # Create a marker file with the current commit + mkdir -p /tmp/last-build-commit + echo "$(git rev-parse HEAD 2>/dev/null || echo ${BITRISE_GIT_COMMIT})" > /tmp/last-build-commit/commit + echo "Build completed successfully at $(date)" >> /tmp/last-build-commit/commit + - save-cache@1: + title: Save last successful build commit marker (Android) + run_if: '{{and (getenv "GITHUB_PR_NUMBER" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - paths: /tmp/last-build-commit + + # Template for E2E Android builds + + _android_e2e_build_template: + before_run: + - code_setup + - set_commit_hash + after_run: + - notify_failure + steps: + - script@1: + title: Generating ccache key using native folder checksum + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/cache/set-cache-envs.sh android + - restore-gradle-cache@2: {} + - install-missing-android-tools@3: + inputs: + - ndk_version: $NDK_VERSION + - gradlew_path: $PROJECT_LOCATION/gradlew + - file-downloader@1: + inputs: + - source: $KEYSTORE_URL + - destination: $KEYSTORE_PATH + run_if: '{{not (enveq "IS_DEV_BUILD" "true")}}' + # Note - This step will fail if stack is not Linux + - script@1: + title: Install CCache, ICU libraries & symlink + inputs: + - content: |- + #!/usr/bin/env bash + sudo apt update + sudo apt install ccache libicu-dev -y + - restore-cache@2: + title: Restore CCache + inputs: + - key: '{{ getenv "CCACHE_KEY" }}' + - script@1: + title: Set skip ccache upload + run_if: '{{ enveq "BITRISE_CACHE_HIT" "exact" }}' + inputs: + - 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 + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + ./scripts/cache/setup-ccache.sh + node -v + export METAMASK_ENVIRONMENT=${METAMASK_ENVIRONMENT:-'dev'} + export METAMASK_BUILD_TYPE=${METAMASK_BUILD_TYPE:-'main'} + IGNORE_BOXLOGS_DEVELOPMENT="true" $BUILD_COMMAND + - save-gradle-cache@1: {} + - save-cache@1: + title: Save CCache + run_if: '{{not (enveq "SKIP_CCACHE_UPLOAD" "true")}}' + inputs: + - key: '{{ getenv "CCACHE_KEY" }}' + - paths: |- + ccache + - script@1: + title: Debug Android build outputs before caching + inputs: + - content: |- + #!/usr/bin/env bash + echo "=== Android Build Output Debug ===" + echo "Checking for Android build outputs to cache..." + + if [ -d "android/app/build/outputs" ]; then + echo "✅ android/app/build/outputs directory exists" + echo "Directory size: $(du -sh android/app/build/outputs 2>/dev/null || echo 'Could not calculate')" + echo "Contents:" + find android/app/build/outputs -type f -name "*.apk" -o -name "*.aab" | head -10 + echo "Total files: $(find android/app/build/outputs -type f | wc -l)" + else + echo "❌ android/app/build/outputs directory does not exist!" + echo "This means the Android build failed or output path is incorrect" + fi + + echo "Current working directory: $(pwd)" + echo "Listing android/app/build directory:" + ls -la android/app/build/ 2>/dev/null || echo "android/app/build does not exist" + - script@1: + title: Prepare Android build outputs for caching + inputs: + - content: |- + #!/usr/bin/env bash + echo "=== Preparing Android cache with proper paths ===" + echo "Current working directory: $(pwd)" + + # Create a clean cache directory structure + mkdir -p /tmp/android-cache/build/outputs + + if [ -d "android/app/build/outputs" ]; then + echo "Copying Android build outputs to cache staging area..." + cp -r android/app/build/outputs/* /tmp/android-cache/build/outputs/ + echo "Cache staging completed" + + echo "Cache contents:" + find /tmp/android-cache -type f | head -10 + echo "Total cache size: $(du -sh /tmp/android-cache 2>/dev/null || echo 'Unknown')" + else + echo "Warning: android/app/build/outputs not found!" + fi + - save-cache@1: + title: Save Android PR Build Cache + inputs: + - key: '{{ getenv "ANDROID_PR_BUILD_CACHE_KEY" }}' + - paths: |- + /tmp/android-cache + - script@1: + title: Save last successful build commit (Android) + run_if: '{{getenv "GITHUB_PR_NUMBER" | ne ""}}' + inputs: + - content: |- + #!/usr/bin/env bash + # Create a marker file with the current commit + mkdir -p /tmp/last-build-commit + echo "$(git rev-parse HEAD 2>/dev/null || echo ${BITRISE_GIT_COMMIT})" > /tmp/last-build-commit/commit + echo "Android build completed successfully at $(date)" >> /tmp/last-build-commit/commit + - save-cache@1: + title: Save last successful build commit marker (Android) + run_if: '{{getenv "GITHUB_PR_NUMBER" | ne ""}}' + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - paths: /tmp/last-build-commit + - deploy-to-bitrise-io@2.2.3: + inputs: + - pipeline_intermediate_files: android/app/build/outputs:INTERMEDIATE_ANDROID_BUILD_DIR + title: Save Android build + - save-cache@1: + title: Save node_modules + inputs: + - key: node_modules-{{ .OS }}-{{ .Arch }}-{{ getenv "BRANCH_COMMIT_HASH" }} + - paths: node_modules + # Actual workflows that inherit from templates + # TODO: Remove this workflow once new build configuration is consolidated + build_android_release: + after_run: + - build_android_main_prod + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_flask_prod: + envs: + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_FLASK_KEYSTORE_URL_URL + - KEYSTORE_PATH: 'android/keystores/flaskRelease.keystore' + - APP_NAME: 'flask' + - OUTPUT_PATH: 'flaskRelease' + - COMMAND_YARN: 'build:android:flask:prod' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_flask_test: + envs: + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_FLASK_UAT_URL + - KEYSTORE_PATH: 'android/keystores/flask-uat.keystore' + - APP_NAME: 'flask' + - OUTPUT_PATH: 'flaskRelease' + - COMMAND_YARN: 'build:android:flask:test' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_flask_e2e: + envs: + - COMMAND_YARN: 'build:android:flask:e2e' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_FLASK_UAT_URL + - KEYSTORE_PATH: 'android/keystores/flask-uat.keystore' + - APP_NAME: 'flask' + - OUTPUT_PATH: 'flaskRelease' + - SHARE_WITH_DETOX: 'true' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_prod: + envs: + - CONFIGURATION: 'Release' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/release.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + - COMMAND_YARN: 'build:android:main:prod' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_beta: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:beta' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_MAIN_RC_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/rc.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_rc: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:rc' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_MAIN_RC_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/rc.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_test: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:test' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_QA_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/internalRelease.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_e2e: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:e2e' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_QA_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/internalRelease.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + - SHARE_WITH_DETOX: 'true' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_exp: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:android:main:exp' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_QA_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/internalRelease.keystore' + - APP_NAME: 'prod' + - OUTPUT_PATH: 'prodRelease' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_release_and_upload_sourcemaps: + envs: + - SENTRY_DISABLE_AUTO_UPLOAD: 'false' + after_run: + - build_android_main_prod + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_rc_and_upload_sourcemaps: + envs: + - SENTRY_DISABLE_AUTO_UPLOAD: 'false' + after_run: + - build_android_main_rc + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_flask_release: + after_run: + - build_android_flask_prod + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_main_dev: + envs: + - CONFIGURATION: 'Debug' + - IS_DEV_BUILD: 'true' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: 'prod' + - BUILD_COMMAND: 'yarn build:android:main:dev' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_devbuild: + after_run: + - build_android_main_dev + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_flask_dev: + envs: + - IS_DEV_BUILD: 'true' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: 'flask' + - BUILD_COMMAND: 'yarn build:android:flask:dev' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_flask_devbuild: + after_run: + - build_android_flask_dev + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_qa_dev: + envs: + - IS_DEV_BUILD: 'true' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: 'qa' + - BUILD_COMMAND: 'yarn build:android:qa:dev' + after_run: + - _android_e2e_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_qa_devbuild: + after_run: + - build_android_qa_dev + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + build_android_qa_prod: + envs: + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - KEYSTORE_FILE_PATH: $BITRISEIO_ANDROID_QA_KEYSTORE_URL + - KEYSTORE_PATH: 'android/keystores/internalRelease.keystore' + - APP_NAME: 'qa' + - OUTPUT_PATH: 'qaRelease' + - COMMAND_YARN: 'build:android:qa:prod' + after_run: + - _android_build_template + meta: + bitrise.io: + stack: linux-docker-android-22.04 + machine_type_id: elite-xl + # TODO: Remove this workflow once new build configuration is consolidated + build_android_qa: + after_run: + - build_android_qa_prod + - _upload_apk_to_browserstack_qa + _upload_apk_to_browserstack_flask: + steps: + - script@1: + title: Upload Flask APK to Browserstack + inputs: + - content: |- + #!/usr/bin/env bash + set -e + set -x + set -o pipefail + APK_PATH=$PROJECT_LOCATION/app/build/outputs/apk/flask/release/app-flask-release.apk + CUSTOM_ID="flask-$BITRISE_GIT_BRANCH-$FLASK_VERSION_NAME-$FLASK_VERSION_NUMBER" + CUSTOM_ID=${CUSTOM_ID////-} + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@$APK_PATH" -F 'data={"custom_id": "'$CUSTOM_ID'"}' | jq -j '.app_url' | envman add --key BROWSERSTACK_ANDROID_FLASK_APP_URL + APK_PATH_FOR_APP_LIVE=$PROJECT_LOCATION/app/build/outputs/apk/flask/release/"$CUSTOM_ID".apk + cp "$APK_PATH" "$APK_PATH_FOR_APP_LIVE" + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@$APK_PATH_FOR_APP_LIVE" -F 'data={"custom_id": "'$CUSTOM_ID'"}' + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X GET https://api-cloud.browserstack.com/app-automate/recent_apps | jq > browserstack_uploaded_flask_apps.json + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: $BITRISE_SOURCE_DIR/browserstack_uploaded_flask_apps.json:BROWSERSTACK_UPLOADED_FLASK_APPS_LIST + title: Save Browserstack uploaded Flask apps JSON + _upload_apk_to_browserstack_qa: + steps: + - script@1: + title: Upload APK to Browserstack + inputs: + - content: |- + #!/usr/bin/env bash + set -e + set -x + set -o pipefail + APK_DIR="$PROJECT_LOCATION/app/build/outputs/apk/qa/release" + ORIGINAL_APK="$APK_DIR/app-qa-release.apk" + + CUSTOM_ID="$BITRISE_GIT_BRANCH-$VERSION_NAME-$VERSION_NUMBER" + CUSTOM_ID=${CUSTOM_ID////-} + + cp "$ORIGINAL_APK" "$APK_DIR/$CUSTOM_ID.apk" + APK_PATH="$APK_DIR/$CUSTOM_ID.apk" + + # Upload to app-automate + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ + -F "file=@$APK_PATH" \ + -F 'data={"custom_id": "'$CUSTOM_ID'"}' \ + | jq -j '.app_url' \ + | envman add --key BROWSERSTACK_ANDROID_APP_URL + + # Upload to app-live + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-live/upload" \ + -F "file=@$APK_PATH" \ + -F 'data={"custom_id": "'$CUSTOM_ID'"}' + + # Get recent apps + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X GET https://api-cloud.browserstack.com/app-automate/recent_apps \ + | jq > browserstack_uploaded_apps.json + - share-pipeline-variable@1: + title: Persist BROWSERSTACK_ANDROID_APP_URL across all stages + inputs: + - variables: |- + BROWSERSTACK_ANDROID_APP_URL + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: $BITRISE_SOURCE_DIR/browserstack_uploaded_apps.json:BROWSERSTACK_UPLOADED_APPS_LIST + title: Save Browserstack uploaded apps JSON + deploy_android_to_store: + steps: + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + - google-play-deploy: + inputs: + - app_path: $BITRISE_PLAY_STORE_ABB_PATH + - track: internal + - service_account_json_key_path: $BITRISEIO_BITRISEIO_SERVICE_ACCOUNT_JSON_KEY_URL_URL + - package_name: $MM_ANDROID_PACKAGE_NAME + envs: + - opts: + is_expand: true + MM_ANDROID_PACKAGE_NAME: io.metamask + deploy_ios_to_store: + steps: + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + - deploy-to-itunesconnect-application-loader@1: + inputs: + - ipa_path: $BITRISE_APP_STORE_IPA_PATH + # iOS Builds + _ios_build_template: + before_run: + - code_setup + - extract_version_info + after_run: + - notify_failure + steps: + - 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 + inputs: + - content: |- + #!/usr/bin/env bash + echo 'This is the current build type: $METAMASK_BUILD_TYPE' + if [ -n "$COMMAND_YARN" ]; then + GIT_BRANCH=$BITRISE_GIT_BRANCH yarn "$COMMAND_YARN" + else + echo "No COMMAND_YARN provided" + exit 1 + fi + - deploy-to-bitrise-io@2.2.3: + title: Share Detox files between pipelines + run_if: '{{getenv "SHARE_WITH_DETOX" | eq "true"}}' + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: |- + ios/build/Build/Products/$CONFIGURATION-iphonesimulator:INTERMEDIATE_IOS_BUILD_DIR + ../Library/Detox/ios:INTERMEDIATE_IOS_DETOX_DIR + - script@1: + title: Rename iOS artifact files + inputs: + - content: |- + #!/usr/bin/env bash + set -ex + + # Set base paths + if [ "$IS_SIM_BUILD" = "true" ]; then + BUILD_DIR="ios/build/Build/Products/${CONFIGURATION}-iphonesimulator" + DEVICE_TYPE="simulator" + BINARY_EXTENSION=".app" + else + BUILD_DIR="ios/build/output" + DEVICE_TYPE="device" + BINARY_EXTENSION=".ipa" + fi + + # Generate new name based on build type and version + if [ -n "$COMMAND_YARN" ]; then + NAME_FROM_YARN_COMMAND="$(cut -d':' -f3- <<< "$COMMAND_YARN" | sed 's/:/-/g')" + NEW_BASE_NAME="metamask-${DEVICE_TYPE}-${NAME_FROM_YARN_COMMAND}-${APP_BUILD_NUMBER}" + else + NEW_BASE_NAME="metamask-${DEVICE_TYPE}-${METAMASK_ENVIRONMENT}-${METAMASK_BUILD_TYPE}-${APP_SEM_VER_NAME}-${APP_BUILD_NUMBER}" + fi + + # Copy binary with new name (preserve original) + OLD_BINARY="$BUILD_DIR/$APP_NAME$BINARY_EXTENSION" + NEW_BINARY="$BUILD_DIR/$NEW_BASE_NAME$BINARY_EXTENSION" + # Need to copy recursively so that .app files are fully copied + cp -r "$OLD_BINARY" "$NEW_BINARY" + + # Copy xcarchive with new name (only for non-simulator builds, preserve original) + if [ "$IS_SIM_BUILD" != "true" ]; then + ARCHIVE_DIR="ios/build" + OLD_ARCHIVE="$ARCHIVE_DIR/$APP_NAME.xcarchive" + NEW_ARCHIVE="$ARCHIVE_DIR/$NEW_BASE_NAME.xcarchive" + cp -r "$OLD_ARCHIVE" "$NEW_ARCHIVE" + fi + + # Export new names as environment variables + envman add --key RENAMED_ARCHIVE_FILE --value "$NEW_BASE_NAME.xcarchive" + envman add --key BINARY_DEPLOY_PATH --value "$BUILD_DIR/$NEW_BASE_NAME$BINARY_EXTENSION" + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: $BINARY_DEPLOY_PATH:BITRISE_APP_STORE_IPA_PATH + - deploy_path: $BINARY_DEPLOY_PATH + - is_compress: true + title: Deploy iOS Binary + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_SIM_BUILD" "true")}}' # Only run for physical builds + inputs: + - deploy_path: ios/build/$RENAMED_ARCHIVE_FILE + title: Deploy Symbols File + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + run_if: '{{not (enveq "IS_SIM_BUILD" "true")}}' # Only run for physical builds + inputs: + - pipeline_intermediate_files: sourcemaps/ios/index.js.map:BITRISE_APP_STORE_SOURCEMAP_PATH + - deploy_path: sourcemaps/ios/index.js.map + title: Deploy Source Map + - save-cache@1: + title: Save iOS PR Build Cache + run_if: '{{and (getenv "IOS_PR_BUILD_CACHE_KEY" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - key: '{{ getenv "IOS_PR_BUILD_CACHE_KEY" }}' + - paths: |- + ios/build/Build/Products/Release-iphonesimulator + ../Library/Detox/ios + - script@1: + title: Save last successful build commit + run_if: '{{and (getenv "GITHUB_PR_NUMBER" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - content: |- + #!/usr/bin/env bash + # Create a marker file with the current commit + mkdir -p /tmp/last-build-commit + echo "$(git rev-parse HEAD 2>/dev/null || echo ${BITRISE_GIT_COMMIT})" > /tmp/last-build-commit/commit + echo "Build completed successfully at $(date)" >> /tmp/last-build-commit/commit + - save-cache@1: + title: Save last successful build commit marker + run_if: '{{and (getenv "GITHUB_PR_NUMBER" | ne "") (getenv "SHARE_WITH_DETOX" | eq "true")}}' + inputs: + - key: 'last-e2e-build-commit-pr-{{ getenv "GITHUB_PR_NUMBER" }}' + - paths: /tmp/last-build-commit + meta: + bitrise.io: + stack: osx-xcode-26.2.x + machine_type_id: g2.mac.4large + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_release: + after_run: + - build_ios_main_prod + build_ios_main_prod: + envs: + - CONFIGURATION: 'Release' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + - COMMAND_YARN: 'build:ios:main:prod' + after_run: + - _ios_build_template + build_ios_main_beta: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:ios:main:beta' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + after_run: + - _ios_build_template + build_ios_main_rc: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:ios:main:rc' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + after_run: + - _ios_build_template + build_ios_main_exp: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:ios:main:exp' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + after_run: + - _ios_build_template + build_ios_main_test: + envs: + - CONFIGURATION: 'Release' + - COMMAND_YARN: 'build:ios:main:test' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + after_run: + - _ios_build_template + build_ios_main_e2e: + envs: + - CONFIGURATION: 'Release' + - IS_SIM_BUILD: 'true' + - SHARE_WITH_DETOX: 'true' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + - COMMAND_YARN: 'build:ios:main:e2e' + after_run: + - _ios_build_template + build_ios_main_e2e_gns_disabled: + envs: + - CONFIGURATION: 'Release' + - IS_SIM_BUILD: 'true' + - SHARE_WITH_DETOX: 'true' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask" + - INFO_PLIST_NAME: "Info.plist" + - COMMAND_YARN: 'build:ios:main:e2e' + after_run: + - _ios_build_template + build_ios_release_and_upload_sourcemaps: + envs: + - SENTRY_DISABLE_AUTO_UPLOAD: 'false' + after_run: + - build_ios_main_prod + build_ios_rc_and_upload_sourcemaps: + envs: + - SENTRY_DISABLE_AUTO_UPLOAD: 'false' + after_run: + - build_ios_main_rc + build_ios_flask_prod: + envs: + - CONFIGURATION: 'Release' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: "MetaMask-Flask" + - INFO_PLIST_NAME: "MetaMask-Flask-Info.plist" + - COMMAND_YARN: 'build:ios:flask:prod' + after_run: + - _ios_build_template + build_ios_flask_test: + envs: + - CONFIGURATION: 'Release' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: "MetaMask-Flask" + - INFO_PLIST_NAME: "MetaMask-Flask-Info.plist" + - COMMAND_YARN: 'build:ios:flask:test' + after_run: + - _ios_build_template + build_ios_flask_e2e: + envs: + - CONFIGURATION: 'Release' + - IS_SIM_BUILD: 'true' + - SHARE_WITH_DETOX: 'true' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: "MetaMask-Flask" + - INFO_PLIST_NAME: "MetaMask-Flask-Info.plist" + - COMMAND_YARN: 'build:ios:flask:e2e' + after_run: + - _ios_build_template + build_ios_main_dev: + envs: + - CONFIGURATION: 'Debug' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: 'MetaMask' + - INFO_PLIST_NAME: 'Info.plist' + - COMMAND_YARN: 'build:ios:main:dev' + after_run: + - _ios_build_template + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_simbuild: + envs: + - IS_SIM_BUILD: 'true' + after_run: + - build_ios_main_dev + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_devbuild: + after_run: + - build_ios_main_dev + build_ios_flask_dev: + envs: + - CONFIGURATION: 'Debug' + - NAME: $FLASK_VERSION_NAME + - NUMBER: $FLASK_VERSION_NUMBER + - APP_NAME: "MetaMask-Flask" + - INFO_PLIST_NAME: "MetaMask-Flask-Info.plist" + - COMMAND_YARN: 'build:ios:flask:dev' + after_run: + - _ios_build_template + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_flask_devbuild: + after_run: + - build_ios_flask_dev + build_ios_flask_simbuild: + envs: + - IS_SIM_BUILD: 'true' + after_run: + - build_ios_flask_dev + build_ios_qa_dev: + envs: + - CONFIGURATION: 'Debug' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask-QA" + - INFO_PLIST_NAME: "MetaMask-QA-Info.plist" + - COMMAND_YARN: 'build:ios:qa:dev' + after_run: + - _ios_build_template + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_qa_devbuild: + after_run: + - build_ios_qa_dev + build_ios_qa_simbuild: + envs: + - IS_SIM_BUILD: 'true' + after_run: + - build_ios_qa_dev + build_ios_qa_prod: + envs: + - CONFIGURATION: 'Release' + - NAME: $VERSION_NAME + - NUMBER: $VERSION_NUMBER + - APP_NAME: "MetaMask-QA" + - INFO_PLIST_NAME: "MetaMask-QA-Info.plist" + - COMMAND_YARN: 'build:ios:qa:prod' + after_run: + - _ios_build_template + # TODO: Remove this workflow once new build configuration is consolidated + build_ios_qa: + after_run: + - build_ios_qa_prod + - _upload_ipa_to_browserstack_qa + _upload_ipa_to_browserstack_qa: + steps: + - script@1: + title: Upload IPA to Browserstack + inputs: + - content: |- + #!/usr/bin/env bash + set -e + set -x + set -o pipefail + + IPA_DIR="ios/build/output" + ORIGINAL_IPA="$IPA_DIR/MetaMask-QA.ipa" + + CUSTOM_ID="$BITRISE_GIT_BRANCH-$VERSION_NAME-$VERSION_NUMBER" + CUSTOM_ID=${CUSTOM_ID////-} + + cp "$ORIGINAL_IPA" "$IPA_DIR/$CUSTOM_ID.ipa" + IPA_PATH="$IPA_DIR/$CUSTOM_ID.ipa" + + # Upload to app-automate + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ + -F "file=@$IPA_PATH" \ + -F 'data={"custom_id": "'$CUSTOM_ID'"}' \ + | jq -j '.app_url' \ + | envman add --key BROWSERSTACK_IOS_APP_URL + echo "BROWSERSTACK_IOS_APP_URL: $BROWSERSTACK_IOS_APP_URL" + # Upload to app-live + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X POST "https://api-cloud.browserstack.com/app-live/upload" \ + -F "file=@$IPA_PATH" \ + -F 'data={"custom_id": "'$CUSTOM_ID'"}' + + # Get recent apps + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" \ + -X GET https://api-cloud.browserstack.com/app-automate/recent_apps \ + | jq > browserstack_uploaded_apps.json + - share-pipeline-variable@1: + title: Persist BROWSERSTACK_IOS_APP_URL across all stages + inputs: + - variables: |- + BROWSERSTACK_IOS_APP_URL + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - deploy_path: browserstack_uploaded_apps.json + title: Bitrise Deploy Browserstack Uploaded Apps + build_ios_flask_release: + before_run: + - code_setup + after_run: + - notify_failure + steps: + - certificate-and-profile-installer@1: {} + - set-xcode-build-number@1: + inputs: + - build_short_version_string: $FLASK_VERSION_NAME + - build_version: $FLASK_VERSION_NUMBER + - plist_path: $PROJECT_LOCATION_IOS/MetaMask/MetaMask-Flask-Info.plist + - script@1: + inputs: + - content: |- + #!/usr/bin/env bash + node -v + METAMASK_BUILD_TYPE='flask' METAMASK_ENVIRONMENT='production' yarn build:ios:pre-flask + title: iOS Sourcemaps & Build + is_always_run: false + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: ios/build/output/MetaMask-Flask.ipa:BITRISE_APP_STORE_IPA_PATH + - deploy_path: ios/build/output/MetaMask-Flask.ipa + title: Deploy iOS IPA + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - deploy_path: ios/build/MetaMask-Flask.xcarchive:BITRISE_APP_STORE_XCARCHIVE_PATH + title: Deploy Symbols File + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - pipeline_intermediate_files: sourcemaps/ios/index.js.map:BITRISE_APP_STORE_SOURCEMAP_PATH + - deploy_path: sourcemaps/ios/index.js.map + title: Deploy Source Map + _upload_ipa_to_browserstack_flask: + steps: + - script@1: + title: Upload Flask IPA to Browserstack + inputs: + - content: |- + #!/usr/bin/env bash + set -e + set -x + set -o pipefail + CUSTOM_ID="flask-$BITRISE_GIT_BRANCH-$FLASK_VERSION_NAME-$FLASK_VERSION_NUMBER" + CUSTOM_ID=${CUSTOM_ID////-} + IPA_PATH=ios/build/output/MetaMask-Flask.ipa + IPA_PATH_FOR_APP_LIVE=ios/build/output/"$CUSTOM_ID".ipa + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-automate/upload" -F "file=@$IPA_PATH" -F 'data={"custom_id": "'$CUSTOM_ID'"}' | jq -j '.app_url' | envman add --key BROWSERSTACK_IOS_FLASK_APP_URL + cp "$IPA_PATH" "$IPA_PATH_FOR_APP_LIVE" + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X POST "https://api-cloud.browserstack.com/app-live/upload" -F "file=@$IPA_PATH_FOR_APP_LIVE" -F 'data={"custom_id": "'$CUSTOM_ID'"}' + curl -u "$BROWSERSTACK_USERNAME:$BROWSERSTACK_ACCESS_KEY" -X GET https://api-cloud.browserstack.com/app-automate/recent_apps | jq > browserstack_uploaded_flask_apps.json + - share-pipeline-variable@1: + title: Persist BROWSERSTACK_IOS_FLASK_APP_URL across all stages + inputs: + - variables: |- + BROWSERSTACK_IOS_FLASK_APP_URL + - deploy-to-bitrise-io@2.2.3: + is_always_run: false + is_skippable: true + inputs: + - deploy_path: browserstack_uploaded_flask_apps.json + title: Bitrise Deploy Browserstack Uploaded Flask Apps + upload_ios_main_to_testflight: + before_run: + - code_setup + after_run: + - notify_failure + steps: + - pull-intermediate-files@1: + inputs: + - artifact_sources: .* + title: Pull iOS build artifacts + - script@1: + title: Setup App Store Connect API Key + inputs: + - content: |- + #!/usr/bin/env bash + set -e + + ./scripts/setup-app-store-connect-api-key.sh \ + "$BITRISE_APP_STORE_CONNECT_API_KEY_ISSUER_ID" \ + "$BITRISE_APP_STORE_CONNECT_API_KEY_KEY_ID" \ + "$BITRISE_APP_STORE_CONNECT_API_KEY_KEY_CONTENT" + - script@1: + title: Upload to TestFlight via Fastlane + timeout: 2700 + inputs: + - content: |- + #!/usr/bin/env bash + set -e + + bash ./scripts/upload-to-testflight.sh \ + "${BITRISEIO_PIPELINE_TITLE:-Unknown}" \ + "${BITRISE_GIT_BRANCH:-Unknown}" \ + "${BITRISE_APP_STORE_IPA_PATH:-}" + - script@1: + title: Cleanup API Key + is_always_run: true + inputs: + - content: |- + #!/usr/bin/env bash + rm -f ios/AuthKey.p8 + echo "🧹 Cleaned up API key file" + meta: + bitrise.io: + stack: osx-xcode-26.2.x + machine_type_id: g2.mac.4large + set_main_target_workflow: + steps: + - share-pipeline-variable@1: + title: Persist METAMASK_BUILD_TYPE across all stages and workflows + inputs: + - variables: |- + METAMASK_BUILD_TYPE=main + set_flask_target_workflow: + steps: + - share-pipeline-variable@1: + title: Persist METAMASK_BUILD_TYPE across all stages and workflows + inputs: + - variables: |- + METAMASK_BUILD_TYPE=flask + +app: + envs: + - opts: + is_expand: false + MM_NOTIFICATIONS_UI_ENABLED: true + - opts: + is_expand: false + MM_NETWORK_UI_REDESIGN_ENABLED: false + - opts: + is_expand: false + MM_SECURITY_ALERTS_API_ENABLED: true + - opts: + is_expand: false + BRIDGE_USE_DEV_APIS: false + - opts: + is_expand: false + MM_PERPS_ENABLED: true + - opts: + is_expand: false + MM_PERPS_BLOCKED_REGIONS: "US,CA-ON,GB,BE" + - opts: + is_expand: false + MM_PERPS_HIP3_ENABLED: true + - opts: + is_expand: false + MM_PERPS_HIP3_ALLOWLIST_MARKETS: "" + - opts: + is_expand: false + MM_PERPS_HIP3_BLOCKLIST_MARKETS: "" + - opts: + is_expand: false + MM_CHARTING_LIBRARY_URL: 'https://charting-assets.static.metamask.io/tradingview/advanced-charts/v30.1.0/' + - opts: + is_expand: false + MM_MUSD_CONVERSION_FLOW_ENABLED: false + - opts: + is_expand: false + PROJECT_LOCATION: android + - opts: + is_expand: false + NDK_VERSION: 26.1.10909125 + - opts: + is_expand: false + CMAKE_VERSION: '3.22.1' + - opts: + is_expand: false + QA_APK_NAME: app-qa-release + - opts: + is_expand: false + MODULE: app + - opts: + is_expand: false + VARIANT: '' + - opts: + is_expand: false + BITRISE_PROJECT_PATH: ios/MetaMask.xcworkspace + - opts: + is_expand: false + BITRISE_SCHEME: MetaMask + - opts: + is_expand: false + BITRISE_EXPORT_METHOD: enterprise + - opts: + is_expand: false + PROJECT_LOCATION_ANDROID: android + - opts: + is_expand: false + PROJECT_LOCATION_IOS: ios + - opts: + is_expand: false + VERSION_NAME: 7.81.0 + - opts: + is_expand: false + VERSION_NUMBER: 4823 + - opts: + is_expand: false + FLASK_VERSION_NAME: 7.81.0 + - opts: + is_expand: false + FLASK_VERSION_NUMBER: 4823 + - opts: + is_expand: false + ANDROID_APK_LINK: '' + - opts: + is_expand: false + ANDROID_AAP_LINK: '' + - opts: + is_expand: false + IOS_APP_LINK: '' + - opts: + is_expand: false + NVM_VERSION: 0.39.7 + - opts: + is_expand: false + NVM_SHA256SUM: '8e45fa547f428e9196a5613efad3bfa4d4608b74ca870f930090598f5af5f643' + - opts: + is_expand: false + NODE_VERSION: 20.18.0 + - opts: + is_expand: false + YARN_VERSION: 4.14.1 + - opts: + is_expand: false + COREPACK_VERSION: 0.28.0 + - opts: + is_expand: false + SEEDLESS_ONBOARDING_ENABLED: true +meta: + bitrise.io: + stack: osx-xcode-26.2.x + machine_type_id: g2.mac.4large +trigger_map: + # Disable auto RC generation + # - push_branch: release/* + # pipeline: pr_rc_rwy_pipeline + # - push_branch: main + # pipeline: expo_dev_pipeline + # - tag: 'qa-*' + # pipeline: create_qa_builds_pipeline + # - tag: 'v*.*.*' + # pipeline: create_qa_builds_pipeline diff --git a/package.json b/package.json index 86588bd19624..081792342ed9 100644 --- a/package.json +++ b/package.json @@ -183,7 +183,7 @@ "@unrs/resolver-binding-wasm32-wasi": "npm:npm-empty-package@1.0.0", "d3-color": "3.1.0", "napi-postinstall": "npm:npm-empty-package@1.0.0", - "axios": "^1.15.1", + "axios": "^1.16.0", "lodash": "4.18.1", "redux-persist-filesystem-storage/react-native-blob-util": "^0.19.9", "@ethersproject/providers/ws": "^7.5.10", @@ -407,7 +407,7 @@ "@walletconnect/utils": "^2.23.0", "@xmldom/xmldom": "^0.8.13", "asyncstorage-down": "4.2.0", - "axios": "^1.15.0", + "axios": "^1.16.0", "bignumber.js": "^9.0.1", "bitcoin-address-validation": "2.2.3", "bnjs4": "npm:bn.js@^4.12.3", diff --git a/yarn.lock b/yarn.lock index 8148c323f21f..7d1c3a53775e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22781,14 +22781,15 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.15.1": - version: 1.15.2 - resolution: "axios@npm:1.15.2" +"axios@npm:^1.16.0": + version: 1.16.1 + resolution: "axios@npm:1.16.1" dependencies: - follow-redirects: "npm:^1.15.11" + follow-redirects: "npm:^1.16.0" form-data: "npm:^4.0.5" + https-proxy-agent: "npm:^5.0.1" proxy-from-env: "npm:^2.1.0" - checksum: 10/eebbd8cb777316d4252cd994a06ec9fb956ef519214a62dab6c5443ae8b753b5116e9a770502316789e6cdef1101e6aae53b6936d6a3791b2d66d75f4d7d2462 + checksum: 10/9b6218cf96321cfbbf8f160658d695367114bcf4fb62492bdc1ccd647f184b5c71ae400e5ecaaf41079bc561de2ecbaf1fec63f398b3ec53389beff7694df64c languageName: node linkType: hard @@ -30149,13 +30150,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.11": - version: 1.15.11 - resolution: "follow-redirects@npm:1.15.11" +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.16.0": + version: 1.16.0 + resolution: "follow-redirects@npm:1.16.0" peerDependenciesMeta: debug: optional: true - checksum: 10/07372fd74b98c78cf4d417d68d41fdaa0be4dcacafffb9e67b1e3cf090bc4771515e65020651528faab238f10f9b9c0d9707d6c1574a6c0387c5de1042cde9ba + checksum: 10/3fbe3d80b3b544c22705d837aa5d4a0d07a740d913534a2620b0a004c610af4148e3b58723536dd099aaa1c9d3a155964bde9665d6e5cb331460809a1fc572fd languageName: node linkType: hard @@ -35643,7 +35644,7 @@ __metadata: appium-xcuitest-driver: "npm:9.5.0" assert: "npm:^1.5.0" asyncstorage-down: "npm:4.2.0" - axios: "npm:^1.15.0" + axios: "npm:^1.16.0" babel-jest: "npm:^29.7.0" babel-loader: "npm:^9.1.3" babel-plugin-inline-import: "npm:^3.0.0"