Skip to content

Commit

Permalink
M:erge branch 'main' into chore/changelog-release-automation
Browse files Browse the repository at this point in the history
  • Loading branch information
sethkfman committed Jul 3, 2024
2 parents abef73d + 53fc082 commit 7043c2f
Show file tree
Hide file tree
Showing 91 changed files with 5,905 additions and 2,116 deletions.
6 changes: 4 additions & 2 deletions .github/guidelines/LABELING_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ It's essential to ensure that PRs have the appropriate labels before they are co
### Mandatory QA labels:
Every PR shall include one the QA labels below:
- **needs-qa**: If the PR includes a new features, complex testing steps, or large refactors, this label must be added to indicated PR requires a full manual QA prior being merged and added to a release.
- **No QA/E2E only**: If the PR does not require any manual QA effort, this label must be added. However, prior to merging, you must ensure end-to-end test runs in Bitrise are successful.

- **Spot check on release build**: If PR does not require feature QA but needs non-automated verification, this label must be added. Furthermore, when that label is added, you must provide test scenarios in the description section, as well as add screenshots, and or recordings of what was tested.

Once PR has been tested by QA (only if the PR was labeled with `needs-qa`):
To merge your PR one of the following QA labels are required:
- **QA Passed**: If the PR was labeled with `needs-qa`, this label must be added once QA has signed off
- **No QA Needed**: If the PR does not require any QA effort. This label should only be used in case you are updating a README or other files that does not impact the building or runtime of the application.
- **Run E2E Smoke**: This label will kick-off E2E testing and trigger a check to make sure the E2E tests pass.

### Optional labels:
- **regression-develop**: This label can manually be added to a bug report issue at the time of its creation if the bug is present on the development branch, i.e., `main`, but is not yet released in production.
Expand Down
13 changes: 11 additions & 2 deletions .github/scripts/check-pr-has-required-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,29 +50,38 @@ async function main(): Promise<void> {
'DO-NOT-MERGE',
];
let hasTeamLabel = false;
let hasQALabel = false;

// Check pull request has at least required QA label and team label
for (const label of pullRequestLabels) {
if (label.startsWith('team-') || label === externalContributorLabel.name) {
console.log(`PR contains a team label as expected: ${label}`);
hasTeamLabel = true;
}
if (label.includes('Run Smoke E2E') || label.includes('No QA Needed') || label.includes('QA Passed') ) {
console.log(`PR contains a QA label as expected: ${label}`);
hasQALabel = true;
}
if (preventMergeLabels.includes(label)) {
core.setFailed(
`PR cannot be merged because it still contains this label: ${label}`,
);
process.exit(1);
}
if (hasTeamLabel) {
if (hasTeamLabel && hasQALabel) {
return;
}
}

// Otherwise, throw an arror to prevent from merging
// Otherwise, throw an error to prevent from merging
let errorMessage = '';
if (!hasTeamLabel) {
errorMessage += 'No team labels found on the PR. ';
}

if (!hasQALabel) {
errorMessage += 'No \'Run E2E Smoke\' or \'No QA Needed\' or \'QA Passed\' label. ';
}
errorMessage += `Please make sure the PR is appropriately labeled before merging it.\n\nSee labeling guidelines for more detail: https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md`;
core.setFailed(errorMessage);
process.exit(1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
TouchableOpacity,
View,
} from 'react-native';
import { WebView } from 'react-native-webview';
import { WebView } from '@metamask/react-native-webview';

// External dependencies.
import ButtonPrimary from '../../Buttons/Button/variants/ButtonPrimary';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
export const SNAP_INSTALL_FLOW = 'snap-install-flow';
export const SNAP_INSTALL_OK = 'snap-install-ok';
///: END:ONLY_INCLUDE_IF
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import { StyleSheet } from 'react-native';
import { Theme } from '../../../util/theme/models';
import Device from '../../../util/device';
Expand Down Expand Up @@ -37,6 +37,10 @@ const styleSheet = (params: { theme: Theme }) => {
snapCell: {
marginVertical: 16,
},
snapAvatar: {
alignSelf: 'center',
marginTop: 16,
},
snapPermissionContainer: {
maxHeight: 300,
borderWidth: 1,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,50 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import React, { useEffect, useState } from 'react';
import ApprovalModal from '../ApprovalModal';
import useApprovalRequest from '../../Views/confirmations/hooks/useApprovalRequest';
import { ApprovalTypes } from '../../../core/RPCMethods/RPCMethodMiddleware';
import Logger from '../../../util/Logger';
import { SnapInstallState } from './InstallSnapApproval.types';
import {
InstallSnapConnectionRequest,
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
InstallSnapError,
InstallSnapPermissionsRequest,
InstallSnapSuccess,
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
} from './components';
import { SNAP_INSTALL_FLOW } from './InstallSnapApproval.constants';
import { ApprovalRequest } from '@metamask/approval-controller';
import { useSelector } from 'react-redux';
import { selectSnapsMetadata } from '../../../selectors/snaps/snapController';
import {
WALLET_SNAP_PERMISSION_KEY,
stripSnapPrefix,
} from '@metamask/snaps-utils';

const InstallSnapApproval = () => {
const snapsMetadata = useSelector(selectSnapsMetadata);

const [installState, setInstallState] = useState<
SnapInstallState | undefined
>(undefined);
const [isFinished, setIsFinished] = useState<boolean>(false);
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
const [installError, setInstallError] = useState<Error | undefined>(
undefined,
);
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
const { approvalRequest, onConfirm, onReject } = useApprovalRequest();

useEffect(() => {
if (approvalRequest) {
if (approvalRequest.type === ApprovalTypes.REQUEST_PERMISSIONS) {
if (
approvalRequest.type === ApprovalTypes.REQUEST_PERMISSIONS &&
Object.keys(approvalRequest?.requestData?.permissions).includes(
WALLET_SNAP_PERMISSION_KEY,
)
) {
setInstallState(SnapInstallState.Confirm);
} else if (
approvalRequest.type === ApprovalTypes.INSTALL_SNAP &&
Expand All @@ -41,14 +59,11 @@ const InstallSnapApproval = () => {

// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const getSnapName = (request: ApprovalRequest<any>): string => {
const getSnapId = (request: ApprovalRequest<any>): string => {
// We first look for the name inside the snapId approvalRequest data
const snapId = request?.requestData?.snapId;
if (typeof snapId === 'string') {
const colonIndex = snapId.indexOf(':');
if (colonIndex !== -1) {
return snapId.substring(colonIndex + 1);
}
return snapId;
}
// If there is no snapId present in the approvalRequest data, we look for the name inside the snapIds caveat
const snapIdsCaveat =
Expand All @@ -57,15 +72,16 @@ const InstallSnapApproval = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(c: any) => c.type === 'snapIds',
);
// return an empty string if we can't find the snap name in the approvalRequest data
return snapIdsCaveat?.value ? Object.keys(snapIdsCaveat.value)[0] : '';
return Object.keys(snapIdsCaveat.value)[0];
};

const getSnapMetadata = (snapId: string) =>
snapsMetadata[snapId] ?? { name: stripSnapPrefix(snapId) };

if (!approvalRequest) return null;

const onInstallSnapFinished = () => {
setIsFinished(true);
};
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)

const onPermissionsConfirm = async () => {
try {
Expand All @@ -75,54 +91,55 @@ const InstallSnapApproval = () => {
});
setInstallState(SnapInstallState.SnapInstalled);
} catch (error) {
Logger.error(
error as Error,
`${SNAP_INSTALL_FLOW} Failed to install snap`,
);
setInstallError(error as Error);
setInstallState(SnapInstallState.SnapInstallError);
}
};
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)

if (!approvalRequest) return null;
if (!approvalRequest || installState === undefined) return null;

const snapName = getSnapName(approvalRequest);
const snapId = getSnapId(approvalRequest);
const snapName = getSnapMetadata(snapId).name;

// TODO: This component should support connecting to multiple Snaps at once.
const renderModalContent = () => {
switch (installState) {
case SnapInstallState.Confirm:
return (
<InstallSnapConnectionRequest
approvalRequest={approvalRequest}
snapId={snapId}
snapName={snapName}
onConfirm={onConfirm}
onCancel={onReject}
/>
);
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
case SnapInstallState.AcceptPermissions:
return (
<InstallSnapPermissionsRequest
approvalRequest={approvalRequest}
snapId={snapId}
snapName={snapName}
onConfirm={onPermissionsConfirm}
onCancel={onReject}
/>
);
case SnapInstallState.SnapInstalled:
return (
<InstallSnapSuccess
snapName={snapName}
onConfirm={onInstallSnapFinished}
/>
);
return <InstallSnapSuccess snapName={snapName} onConfirm={onConfirm} />;
case SnapInstallState.SnapInstallError:
return (
<InstallSnapError
snapName={snapName}
onConfirm={onInstallSnapFinished}
onConfirm={onConfirm}
error={installError}
/>
);
///: END:ONLY_INCLUDE_IF
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
default:
return null;
}
Expand All @@ -131,10 +148,7 @@ const InstallSnapApproval = () => {
const content = renderModalContent();

return content ? (
<ApprovalModal
isVisible={installState !== undefined && !isFinished}
onCancel={onReject}
>
<ApprovalModal isVisible={installState !== undefined} onCancel={onReject}>
{content}
</ApprovalModal>
) : null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
interface InstallSnapFlowProps {
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
approvalRequest: any;
snapId: string;
snapName: string;
onConfirm: () => void;
onCancel: () => void;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import React, { useMemo } from 'react';
import { ImageSourcePropType, View } from 'react-native';
import { View } from 'react-native';
import { InstallSnapFlowProps } from '../../InstallSnapApproval.types';
import styleSheet from '../../InstallSnapApproval.styles';
import { strings } from '../../../../../../locales/i18n';
Expand All @@ -11,10 +11,6 @@ import Text, {
import TagUrl from '../../../../../component-library/components/Tags/TagUrl';
import { getUrlObj, prefixUrlWithProtocol } from '../../../../../util/browser';
import { IconName } from '../../../../../component-library/components/Icons/Icon';
import Cell, {
CellVariant,
} from '../../../../../component-library/components/Cells/Cell';
import { AvatarVariant } from '../../../../../component-library/components/Avatars/Avatar';
import {
ButtonSize,
ButtonVariants,
Expand All @@ -29,15 +25,18 @@ import {
SNAP_INSTALL_CONNECT,
SNAP_INSTALL_CONNECTION_REQUEST,
} from './InstallSnapConnectionRequest.constants';
import { useFavicon } from '../../../../hooks/useFavicon';
import { SnapAvatar } from '../../../../UI/Snaps/SnapAvatar/SnapAvatar';

const InstallSnapConnectionRequest = ({
approvalRequest,
snapId,
snapName,
onConfirm,
onCancel,
}: Pick<
InstallSnapFlowProps,
'approvalRequest' | 'onConfirm' | 'onCancel' | 'snapName'
'approvalRequest' | 'onConfirm' | 'onCancel' | 'snapId' | 'snapName'
>) => {
const { styles } = useStyles(styleSheet, {});

Expand All @@ -46,10 +45,7 @@ const InstallSnapConnectionRequest = ({
[approvalRequest.origin],
);

const favicon: ImageSourcePropType = useMemo(() => {
const iconUrl = `https://api.faviconkit.com/${origin}/50`;
return { uri: iconUrl };
}, [origin]);
const favicon = useFavicon(origin);

const urlWithProtocol = prefixUrlWithProtocol(origin);

Expand Down Expand Up @@ -85,22 +81,18 @@ const InstallSnapConnectionRequest = ({
label={urlWithProtocol}
iconName={secureIcon}
/>
<SnapAvatar
snapId={snapId}
snapName={snapName}
style={styles.snapAvatar}
/>
<SheetHeader title={strings('install_snap.title')} />
<Text style={styles.description} variant={TextVariant.BodyMD}>
{strings('install_snap.description', {
origin,
snap: snapName,
})}
</Text>
<Cell
style={styles.snapCell}
variant={CellVariant.Display}
title={snapName}
avatarProps={{
variant: AvatarVariant.Icon,
name: IconName.Snaps,
}}
/>
<View style={styles.actionContainer}>
<BottomSheetFooter
buttonsAlignment={ButtonsAlignment.Horizontal}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
///: BEGIN:ONLY_INCLUDE_IF(external-snaps)
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
/* eslint-disable import/prefer-default-export */
import InstallSnapConnectionRequest from './InstallSnapConnectionRequest';

Expand Down
Loading

0 comments on commit 7043c2f

Please sign in to comment.