diff --git a/.ruby-version b/.ruby-version index b5021469305..75a22a26ac4 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.0.2 +3.0.3 diff --git a/android/.project b/android/.project index 3cf8618bf4c..1efd2443546 100644 --- a/android/.project +++ b/android/.project @@ -14,4 +14,15 @@ org.eclipse.buildship.core.gradleprojectnature + + + 1684448150858 + + 30 + + org.eclipse.core.resources.regexFilterMatcher + node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ + + + diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs index e8895216fd3..e479558406c 100644 --- a/android/.settings/org.eclipse.buildship.core.prefs +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -1,2 +1,13 @@ +arguments= +auto.sync=false +build.scans.enabled=false +connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) connection.project.dir= eclipse.preferences.version=1 +gradle.user.home= +java.home= +jvm.arguments= +offline.mode=false +override.workspace.settings=false +show.console.view=false +show.executions.view=false diff --git a/android/app/build.gradle b/android/app/build.gradle index 998c8114719..4e2abb24ca9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -202,6 +202,12 @@ android { storePassword 'android' keyAlias 'androiddebugkey' keyPassword 'android' + } + flask { + storeFile file('../keystores/flaskRelease.keystore') + storePassword System.getenv("BITRISEIO_ANDROID_FLASK_KEYSTORE_PASSWORD") + keyAlias System.getenv("BITRISEIO_ANDROID_FLASK_KEYSTORE_ALIAS") + keyPassword System.getenv("BITRISEIO_ANDROID_FLASK_KEYSTORE_PRIVATE_KEY_PASSWORD") } } @@ -246,6 +252,14 @@ android { applicationId "io.metamask" signingConfig signingConfigs.release } + flask { + dimension "version" + versionName "0.0.3" + versionCode 1128 + applicationIdSuffix ".flask" + applicationId "io.metamask" + signingConfig signingConfigs.flask + } } buildTypes.each { @@ -288,6 +302,10 @@ dependencies { androidTestImplementation 'junit:junit:4.12' implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "com.android.installreferrer:installreferrer:2.2" + implementation 'org.apache.commons:commons-compress:1.22' + androidTestImplementation 'org.mockito:mockito-android:4.2.0' + androidTestImplementation 'androidx.test:core:1.5.0' + androidTestImplementation 'androidx.test:core-ktx:1.5.0' debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { exclude group:'com.facebook.fbjni' diff --git a/android/app/src/androidTest/java/com/metamask/nativeModules/RNTarTest/RNTarTest.java b/android/app/src/androidTest/java/com/metamask/nativeModules/RNTarTest/RNTarTest.java new file mode 100644 index 00000000000..710854be4ca --- /dev/null +++ b/android/app/src/androidTest/java/com/metamask/nativeModules/RNTarTest/RNTarTest.java @@ -0,0 +1,52 @@ +package com.metamask.nativeModules.RNTarTest; + +import androidx.test.core.app.ApplicationProvider; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.ReactApplicationContext; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import java.nio.file.StandardCopyOption; + +import io.metamask.nativeModules.RNTar.RNTar; + +@RunWith(JUnit4.class) +public class RNTarTest { + private RNTar tar; + private ReactApplicationContext reactContext; + private Promise promise; + + @Before + public void setUp() { + reactContext = new ReactApplicationContext(ApplicationProvider.getApplicationContext()); + tar = new RNTar(reactContext); + promise = mock(Promise.class); + } + + @Test + public void testUnTar_validTgzFile() throws IOException { + // Prepare a sample .tgz file + InputStream tgzResource = Thread.currentThread().getContextClassLoader().getResourceAsStream("validTgzFile.tgz"); + try { + File tgzFile = new File(reactContext.getCacheDir(), "validTgzFile.tgz"); + Files.copy(tgzResource, tgzFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + String outputPath = reactContext.getCacheDir().getAbsolutePath() + "/output"; + // Call unTar method + tar.unTar(tgzFile.getAbsolutePath(), outputPath, promise); + // Verify the promise was resolved + Path expectedDecompressedPath = Paths.get(outputPath, "package"); + verify(promise).resolve(expectedDecompressedPath.toString()); + } finally { + tgzResource.close(); + } + } +} diff --git a/android/app/src/androidTest/resources/validTgzFile.tgz b/android/app/src/androidTest/resources/validTgzFile.tgz new file mode 100644 index 00000000000..c68944080bf Binary files /dev/null and b/android/app/src/androidTest/resources/validTgzFile.tgz differ diff --git a/android/app/src/flask/res/ic_launcher-playstore.png b/android/app/src/flask/res/ic_launcher-playstore.png new file mode 100644 index 00000000000..e25f87835cf Binary files /dev/null and b/android/app/src/flask/res/ic_launcher-playstore.png differ diff --git a/android/app/src/flask/res/mipmap-hdpi/ic_launcher.png b/android/app/src/flask/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000000..cc78c59ed29 Binary files /dev/null and b/android/app/src/flask/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/flask/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/flask/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..66375b3c3c8 Binary files /dev/null and b/android/app/src/flask/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/flask/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/flask/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000000..890cd7b4e02 Binary files /dev/null and b/android/app/src/flask/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/flask/res/mipmap-mdpi/ic_launcher.png b/android/app/src/flask/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000000..5fb9067a09b Binary files /dev/null and b/android/app/src/flask/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/flask/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/flask/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..fba595c81da Binary files /dev/null and b/android/app/src/flask/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/flask/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/flask/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000000..13963b80f64 Binary files /dev/null and b/android/app/src/flask/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/flask/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/flask/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000000..322085a030f Binary files /dev/null and b/android/app/src/flask/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/flask/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/flask/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..34fc0b75cd6 Binary files /dev/null and b/android/app/src/flask/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/flask/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/flask/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000000..83dded7f3d4 Binary files /dev/null and b/android/app/src/flask/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/flask/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/flask/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000000..a6d7b9dc6a2 Binary files /dev/null and b/android/app/src/flask/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/flask/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/flask/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..6424bfea2ff Binary files /dev/null and b/android/app/src/flask/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/flask/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/flask/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000000..702f488f15f Binary files /dev/null and b/android/app/src/flask/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/flask/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/flask/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000000..09319a8823a Binary files /dev/null and b/android/app/src/flask/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/flask/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/flask/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..37e20fd5424 Binary files /dev/null and b/android/app/src/flask/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/flask/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/flask/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000000..b591d53a15a Binary files /dev/null and b/android/app/src/flask/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/flask/res/values/strings.xml b/android/app/src/flask/res/values/strings.xml new file mode 100644 index 00000000000..94ca908ad97 --- /dev/null +++ b/android/app/src/flask/res/values/strings.xml @@ -0,0 +1,3 @@ + + MetaMask Flask + diff --git a/android/app/src/main/java/io/metamask/MainApplication.java b/android/app/src/main/java/io/metamask/MainApplication.java index fbeafe10a93..e5e9b2e1c3a 100644 --- a/android/app/src/main/java/io/metamask/MainApplication.java +++ b/android/app/src/main/java/io/metamask/MainApplication.java @@ -1,7 +1,6 @@ package io.metamask; import com.facebook.react.ReactApplication; -import com.cmcewen.blurview.BlurViewPackage; import com.brentvatne.react.ReactVideoPackage; import android.content.Context; import com.facebook.react.PackageList; @@ -25,6 +24,7 @@ import java.lang.reflect.Field; import com.facebook.react.bridge.JSIModulePackage; import com.swmansion.reanimated.ReanimatedJSIModulePackage; +import io.metamask.nativeModules.RNTar.RNTarPackage; public class MainApplication extends MultiDexApplication implements ShareApplication, ReactApplication { @@ -43,7 +43,7 @@ protected List getPackages() { packages.add(new RCTAnalyticsPackage()); packages.add(new PreventScreenshotPackage()); packages.add(new ReactVideoPackage()); - + packages.add(new RNTarPackage()); return packages; } diff --git a/android/app/src/main/java/io/metamask/nativeModules/RNTar/RNTar.java b/android/app/src/main/java/io/metamask/nativeModules/RNTar/RNTar.java new file mode 100644 index 00000000000..1c5a4fbf297 --- /dev/null +++ b/android/app/src/main/java/io/metamask/nativeModules/RNTar/RNTar.java @@ -0,0 +1,138 @@ +package io.metamask.nativeModules.RNTar; + +import androidx.annotation.NonNull; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import android.util.Log; +import com.facebook.react.bridge.Promise; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.zip.GZIPInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; +import org.apache.commons.compress.archivers.tar.TarArchiveEntry; +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.Path; +import android.os.Build; + +public class RNTar extends ReactContextBaseJavaModule { + private static String MODULE_NAME = "RNTar"; + + public RNTar(ReactApplicationContext context) { + super(context); + } + + @NonNull + @Override + public String getName() { + return MODULE_NAME; + } + + private void createDirectories(String path) throws IOException { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Files.createDirectories(Paths.get(path)); + } else { + File dir = new File(path); + if (!dir.exists()) { + dir.mkdirs(); + } + } + } + + private boolean isReadableWritable(String path) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Path dirPath = Paths.get(path); + return Files.isReadable(dirPath) && Files.isWritable(dirPath); + } else { + File dir = new File(path); + return dir.canRead() && dir.canWrite(); + } + } + + private boolean exists(String path) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Files.exists(Paths.get(path)); + } else { + return new File(path).exists(); + } + } + + private String extractTgzFile(String tgzPath, String outputPath) throws IOException { + try { + // Check if .tgz file exists + if (!exists(tgzPath)) { + throw new IOException("The specified .tgz file does not exist."); + } + + // Create output directory if it doesn't exist + createDirectories(outputPath); + + // Check if the output directory is readable and writable + if (!isReadableWritable(outputPath)) { + throw new IOException("The output directory is not readable and/or writable."); + } + + // Set up the input streams for reading the .tgz file + try (FileInputStream fileInputStream = new FileInputStream(tgzPath); + GZIPInputStream gzipInputStream = new GZIPInputStream(fileInputStream); + TarArchiveInputStream tarInputStream = new TarArchiveInputStream(new BufferedInputStream(gzipInputStream))) { + + TarArchiveEntry entry; + + // Loop through the entries in the .tgz file + while ((entry = (TarArchiveEntry) tarInputStream.getNextEntry()) != null) { + File outputFile = new File(outputPath, entry.getName()); + + // If it is a directory, create the output directory + if (entry.isDirectory()) { + createDirectories(outputFile.getAbsolutePath()); + } else { + // Create parent directories if they don't exist + createDirectories(outputFile.getParent()); + + // Set up the output streams for writing the file + try (FileOutputStream fos = new FileOutputStream(outputFile); + BufferedWriter dest = new BufferedWriter(new OutputStreamWriter(fos, StandardCharsets.UTF_8))) { + + // Set up a BufferedReader for reading the file from the .tgz file + BufferedReader tarReader = new BufferedReader(new InputStreamReader(tarInputStream, StandardCharsets.UTF_8)); + + // Read the file line by line and convert line endings to the system default + String line; + while ((line = tarReader.readLine()) != null) { + dest.write(line); + dest.newLine(); + } + } + } + } + } + // Return the output directory path + return new File(outputPath, "package").getAbsolutePath(); + } catch (IOException e) { + Log.e("DecompressTgzFile", "Error decompressing tgz file", e); + throw new IOException("Error decompressing tgz file: " + e.getMessage(), e); + } + } + + @ReactMethod + public void unTar(String pathToRead, String pathToWrite, final Promise promise) { + Log.d(MODULE_NAME, "Create event called with name: " + pathToRead + + " and location: " + pathToWrite); + try { + String decompressedPath = extractTgzFile(pathToRead, pathToWrite); + promise.resolve(decompressedPath); + } catch(Exception e) { + promise.reject("Error uncompressing file:", e); + } + } +} diff --git a/android/app/src/main/java/io/metamask/nativeModules/RNTar/RNTarPackage.java b/android/app/src/main/java/io/metamask/nativeModules/RNTar/RNTarPackage.java new file mode 100644 index 00000000000..b121a633631 --- /dev/null +++ b/android/app/src/main/java/io/metamask/nativeModules/RNTar/RNTarPackage.java @@ -0,0 +1,28 @@ +package io.metamask.nativeModules.RNTar; +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class RNTarPackage implements ReactPackage { + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new RNTar(reactContext)); + + return modules; + } + +} diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 4ae7d12378f..64c9c4a3bbe 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,4 @@ - - \ No newline at end of file + diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index e7dc3164eda..2a7aa50bd54 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -32,7 +32,7 @@ import branch from 'react-native-branch'; import AppConstants from '../../../core/AppConstants'; import Logger from '../../../util/Logger'; import { trackErrorAsAnalytics } from '../../../util/analyticsV2'; -import { routingInstrumentation } from '../../../util/sentryUtils'; +import { routingInstrumentation } from '../../../util/sentry/sentryUtils'; import Analytics from '../../../core/Analytics/Analytics'; import { connect, useSelector, useDispatch } from 'react-redux'; import { diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js index a5f1488be7c..9311e8e8c7f 100644 --- a/app/components/Nav/Main/MainNavigator.js +++ b/app/components/Nav/Main/MainNavigator.js @@ -59,6 +59,8 @@ import { colors as importedColors } from '../../../styles/common'; import OrderDetails from '../../UI/FiatOnRampAggregator/Views/OrderDetails'; import TabBar from '../../../component-library/components/Navigation/TabBar'; import BrowserUrlModal from '../../Views/BrowserUrlModal'; +import { SnapsSettingsList } from '../../Views/Snaps/SnapsSettingsList'; +import { SnapSettings } from '../../Views/Snaps/SnapSettings'; import Routes from '../../../constants/navigation/Routes'; import AnalyticsV2 from '../../../util/analyticsV2'; import { MetaMetricsEvents } from '../../../core/Analytics'; @@ -198,6 +200,21 @@ const BrowserFlow = () => ( export const DrawerContext = React.createContext({ drawerRef: null }); +const SnapsSettingsStack = () => ( + + + + +); + const SettingsFlow = () => ( ( component={EnterPasswordSimple} options={EnterPasswordSimple.navigationOptions} /> + ); diff --git a/app/components/Nav/Main/RootRPCMethodsUI.js b/app/components/Nav/Main/RootRPCMethodsUI.js index daf884a945c..a4707d43422 100644 --- a/app/components/Nav/Main/RootRPCMethodsUI.js +++ b/app/components/Nav/Main/RootRPCMethodsUI.js @@ -57,6 +57,7 @@ import { selectProviderType, } from '../../../selectors/networkController'; import { createAccountConnectNavDetails } from '../../Views/AccountConnect'; +import { InstallSnapApprovalFlow } from '../../UI/InstallSnapApprovalFlow'; const hstInterface = new ethers.utils.Interface(abi); @@ -85,6 +86,8 @@ const RootRPCMethodsUI = (props) => { const [signMessageParams, setSignMessageParams] = useState(undefined); + const [installSnap, setInstallSnap] = useState(false); + const setTransactionObject = props.setTransactionObject; const setEtherTransaction = props.setEtherTransaction; @@ -665,6 +668,47 @@ const RootRPCMethodsUI = (props) => { /> ); + const onInstallSnapConfirm = () => { + acceptPendingApproval(hostToApprove.id, hostToApprove.requestData); + }; + + const onInstallSnapFinished = () => { + setShowPendingApproval(false); + setInstallSnap(false); + }; + + const onInstallSnapReject = () => { + rejectPendingApproval(hostToApprove.id, hostToApprove.requestData); + setShowPendingApproval(false); + setInstallSnap(false); + }; + + /** + * Render the modal that asks the user to approve/reject connections to a dapp using the MetaMask SDK. + */ + const renderInstallSnapApprovalModal = () => ( + + + + ); + // unapprovedTransaction effect useEffect(() => { Engine.context.TransactionController.hub.on( @@ -691,26 +735,38 @@ const RootRPCMethodsUI = (props) => { } switch (request.type) { + case ApprovalTypes.INSTALL_SNAP: + setHostToApprove({ requestData, id: request.id }); + showPendingApprovalModal({ + type: ApprovalTypes.INSTALL_SNAP, + origin: request.origin, + }); + setInstallSnap(true); + break; + case ApprovalTypes.UPDATE_SNAP: + // eslint-disable-next-line no-console + console.log('Update Snap'); + break; case ApprovalTypes.REQUEST_PERMISSIONS: - if (requestData?.permissions?.eth_accounts) { - const { - metadata: { id }, - } = requestData; - - const totalAccounts = props.accountsLength; - - AnalyticsV2.trackEvent(MetaMetricsEvents.CONNECT_REQUEST_STARTED, { - number_of_accounts: totalAccounts, - source: 'PERMISSION SYSTEM', - }); - - props.navigation.navigate( - ...createAccountConnectNavDetails({ - hostInfo: requestData, - permissionRequestId: id, - }), - ); - } + // eslint-disable-next-line no-case-declarations + const { + metadata: { id }, + } = requestData; + + // eslint-disable-next-line no-case-declarations + const totalAccounts = props.accountsLength; + + AnalyticsV2.trackEvent(MetaMetricsEvents.CONNECT_REQUEST_STARTED, { + number_of_accounts: totalAccounts, + source: 'PERMISSION SYSTEM', + }); + + props.navigation.navigate( + ...createAccountConnectNavDetails({ + hostInfo: requestData, + permissionRequestId: id, + }), + ); break; case ApprovalTypes.CONNECT_ACCOUNTS: setHostToApprove({ data: requestData, id: request.id }); @@ -800,6 +856,7 @@ const RootRPCMethodsUI = (props) => { {renderWatchAssetModal()} {renderQRSigningModal()} {renderAccountsApprovalModal()} + {renderInstallSnapApprovalModal()} ); }; diff --git a/app/components/Nav/Main/index.js b/app/components/Nav/Main/index.js index 2839c2a276b..b5b3d9aef71 100644 --- a/app/components/Nav/Main/index.js +++ b/app/components/Nav/Main/index.js @@ -68,6 +68,7 @@ import { selectProviderConfig, selectProviderType, } from '../../../selectors/networkController'; +import { SnapsExecutionWebView } from '../../UI/SnapsExecutionWebView'; const Stack = createStackNavigator(); @@ -335,6 +336,9 @@ const Main = (props) => { ) : ( renderLoader() )} + + + diff --git a/app/components/UI/InstallSnapApprovalFlow/InstallSnapApprovalFlow.tsx b/app/components/UI/InstallSnapApprovalFlow/InstallSnapApprovalFlow.tsx new file mode 100644 index 00000000000..4b2ce175874 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/InstallSnapApprovalFlow.tsx @@ -0,0 +1,99 @@ +import React, { useCallback, useState } from 'react'; +import { + InstallSnapApprovalArgs, + SnapInstallState, +} from './InstallSnapApprovalFlow.types'; +import { InstallSnapConnectionRequest } from './components/InstallSnapConnectionRequest'; +import { View } from 'react-native'; +import { InstallSnapPermissionsRequest } from './components/InstallSnapPermissionsRequest'; +import { InstallSnapSuccess } from './components/InstallSnapSuccess'; +import { InstallSnapError } from './components/InstallSnapError'; +import { SNAP_INSTALL_FLOW } from '../../../constants/test-ids'; +import Logger from '../../../util/Logger'; + +const InstallSnapApprovalFlow = ({ + requestData, + onConfirm, + onFinish, + onCancel, +}: InstallSnapApprovalArgs) => { + const [installState, setInstallState] = useState( + SnapInstallState.Confirm, + ); + + const [installError, setInstallError] = useState( + undefined, + ); + + const onConfirmNext = useCallback(() => { + setInstallState(SnapInstallState.AcceptPermissions); + }, []); + + const onPermissionsConfirm = useCallback(() => { + try { + onConfirm(); + } catch (error) { + Logger.error( + error as Error, + `${SNAP_INSTALL_FLOW} Failed to install snap`, + ); + setInstallError(error as Error); + setInstallState(SnapInstallState.SnapInstallError); + } + setInstallState(SnapInstallState.SnapInstalled); + }, [onConfirm]); + + const onSnapInstalled = useCallback(() => { + onFinish(); + }, [onFinish]); + + const renderInstallStep = useCallback(() => { + switch (installState) { + case SnapInstallState.Confirm: + return ( + + ); + case SnapInstallState.AcceptPermissions: + return ( + + ); + case SnapInstallState.SnapInstalled: + return ( + + ); + case SnapInstallState.SnapInstallError: + return ( + + ); + } + }, [ + installError, + installState, + onCancel, + onConfirmNext, + onPermissionsConfirm, + onSnapInstalled, + requestData, + ]); + + return {renderInstallStep()}; +}; + +export default InstallSnapApprovalFlow; diff --git a/app/components/UI/InstallSnapApprovalFlow/InstallSnapApprovalFlow.types.ts b/app/components/UI/InstallSnapApprovalFlow/InstallSnapApprovalFlow.types.ts new file mode 100644 index 00000000000..24be7bc307f --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/InstallSnapApprovalFlow.types.ts @@ -0,0 +1,25 @@ +interface InstallSnapApprovalArgs { + requestData: any; + onConfirm: () => void; + onFinish: () => void; + onCancel: () => void; + chainId?: string; +} + +interface InstallSnapFlowProps { + requestData: any; + onConfirm: () => void; + onCancel: () => void; + chainId?: string; + error?: Error; +} + +export enum SnapInstallState { + Confirm = 'Confirm', + AcceptPermissions = 'AcceptPermissions', + SnapInstalled = 'SnapInstalled', + SnapInstallError = 'SnapInstallError', +} + +// eslint-disable-next-line import/prefer-default-export +export type { InstallSnapApprovalArgs, InstallSnapFlowProps }; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/InstallSnapConnectionRequest.styles.ts b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/InstallSnapConnectionRequest.styles.ts new file mode 100644 index 00000000000..3d71b9b0214 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/InstallSnapConnectionRequest.styles.ts @@ -0,0 +1,35 @@ +import { StyleSheet } from 'react-native'; +import Device from '../../../../../util/device'; +import { Theme } from '../../../../../util/theme/models'; + +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + const { colors } = theme; + return StyleSheet.create({ + root: { + backgroundColor: colors.background.default, + paddingTop: 24, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + minHeight: 200, + paddingBottom: Device.isIphoneX() ? 20 : 0, + }, + accountCardWrapper: { + paddingHorizontal: 24, + }, + actionContainer: { + flex: 0, + paddingVertical: 16, + justifyContent: 'center', + }, + snapCell: { + marginVertical: 16, + }, + description: { + textAlign: 'center', + paddingBottom: 16, + }, + }); +}; + +export default styleSheet; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/InstallSnapConnectionRequest.tsx b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/InstallSnapConnectionRequest.tsx new file mode 100644 index 00000000000..b555926d6c9 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/InstallSnapConnectionRequest.tsx @@ -0,0 +1,118 @@ +import React, { useMemo } from 'react'; +import { ImageSourcePropType, View } from 'react-native'; +import { InstallSnapFlowProps } from '../../InstallSnapApprovalFlow.types'; +import styleSheet from './InstallSnapConnectionRequest.styles'; +import { strings } from '../../../../../../locales/i18n'; +import { + SNAP_INSTALL_CANCEL, + SNAP_INSTALL_CONNECT, + SNAP_INSTALL_CONNECTION_REQUEST, +} from '../../../../../constants/test-ids'; +import SheetHeader from '../../../../../component-library/components/Sheet/SheetHeader'; +import Text, { + TextVariant, +} from '../../../../../component-library/components/Texts/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, { + CellVariants, +} from '../../../../../component-library/components/Cells/Cell'; +import { AvatarVariants } from '../../../../../component-library/components/Avatars/Avatar'; +import { + ButtonSize, + ButtonVariants, +} from '../../../../../component-library/components/Buttons/Button'; +import BottomSheetFooter, { + ButtonsAlignment, +} from '../../../../../component-library/components/BottomSheets/BottomSheetFooter'; +import { ButtonProps } from '../../../../../component-library/components/Buttons/Button/Button.types'; +import { useStyles } from '../../../../hooks/useStyles'; + +const InstallSnapConnectionRequest = ({ + requestData, + onConfirm, + onCancel, +}: InstallSnapFlowProps) => { + const { styles } = useStyles(styleSheet, {}); + + const snapName = useMemo(() => { + const colonIndex = requestData.snapId.indexOf(':'); + if (colonIndex !== -1) { + return requestData.snapId.substring(colonIndex + 1); + } + return requestData.snapId; + }, [requestData.snapId]); + + const dappOrigin = useMemo( + () => requestData.metadata.dappOrigin, + [requestData.metadata.dappOrigin], + ); + + const favicon: ImageSourcePropType = useMemo(() => { + const iconUrl = `https://api.faviconkit.com/${dappOrigin}/50`; + return { uri: iconUrl }; + }, [dappOrigin]); + + const urlWithProtocol = prefixUrlWithProtocol(dappOrigin); + + const secureIcon = useMemo( + () => + (getUrlObj(dappOrigin) as URL).protocol === 'https:' + ? IconName.Lock + : IconName.LockSlash, + [dappOrigin], + ); + + const cancelButtonProps: ButtonProps = { + variant: ButtonVariants.Secondary, + label: strings('accountApproval.cancel'), + size: ButtonSize.Lg, + onPress: onCancel, + testID: SNAP_INSTALL_CANCEL, + }; + + const connectButtonProps: ButtonProps = { + variant: ButtonVariants.Primary, + label: strings('accountApproval.connect'), + size: ButtonSize.Lg, + onPress: onConfirm, + testID: SNAP_INSTALL_CONNECT, + }; + + return ( + + + + + + {strings('install_snap.description', { + origin: dappOrigin, + snap: snapName, + })} + + + + + + + + ); +}; + +export default InstallSnapConnectionRequest; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/index.ts b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/index.ts new file mode 100644 index 00000000000..69c1912f1a4 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/index.ts @@ -0,0 +1,4 @@ +/* eslint-disable import/prefer-default-export */ +import InstallSnapConnectionRequest from './InstallSnapConnectionRequest'; + +export { InstallSnapConnectionRequest }; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/test/InstallSnapConnectionRequest.test.tsx b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/test/InstallSnapConnectionRequest.test.tsx new file mode 100644 index 00000000000..5352b6990cd --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapConnectionRequest/test/InstallSnapConnectionRequest.test.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import InstallSnapConnectionRequest from '../InstallSnapConnectionRequest'; +import { + SNAP_INSTALL_CANCEL, + SNAP_INSTALL_CONNECT, + SNAP_INSTALL_CONNECTION_REQUEST, +} from '../../../../../../constants/test-ids'; + +describe('InstallSnapConnectionRequest', () => { + const requestData = { + metadata: { + id: 'uNadWHqPnwOM4NER3mERI', + origin: 'npm:@lavamoat/tss-snap', + dappOrigin: 'tss.ac', + }, + permissions: { + snap_manageState: {}, + 'endowment:rpc': { + caveats: [ + { + type: 'rpcOrigin', + value: { + dapps: true, + snaps: true, + }, + }, + ], + }, + }, + snapId: 'npm:@lavamoat/tss-snap', + }; + + const onConfirm = jest.fn(); + const onCancel = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const { getByTestId } = render( + , + ); + expect(getByTestId(SNAP_INSTALL_CONNECTION_REQUEST)).toBeDefined(); + }); + + it('calls onConfirm when the connect button is pressed', () => { + const { getByTestId } = render( + , + ); + + fireEvent.press(getByTestId(SNAP_INSTALL_CONNECT)); + expect(onConfirm).toHaveBeenCalledTimes(1); + }); + + it('calls onCancel when the cancel button is pressed', () => { + const { getByTestId } = render( + , + ); + + fireEvent.press(getByTestId(SNAP_INSTALL_CANCEL)); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it('correctly prefixes dappOrigin with protocol', () => { + const { getByText } = render( + , + ); + + const expectedUrl = 'https://tss.ac'; + expect(getByText(expectedUrl)).toBeTruthy(); + }); +}); diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/InstallSnapError.styles.ts b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/InstallSnapError.styles.ts new file mode 100644 index 00000000000..a1a77544a57 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/InstallSnapError.styles.ts @@ -0,0 +1,60 @@ +import { StyleSheet } from 'react-native'; +import { Theme } from '../../../../../util/theme/models'; +import Device from '../../../../../util/device'; + +/** + * + * @param params Style sheet params. + * @param params.theme App theme from ThemeContext. + * @param params.vars Inputs that the style sheet depends on. + * @returns StyleSheet object. + */ +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + const { colors } = theme; + return StyleSheet.create({ + root: { + backgroundColor: colors.background.default, + paddingTop: 24, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + minHeight: 200, + paddingBottom: Device.isIphoneX() ? 20 : 0, + }, + accountCardWrapper: { + paddingHorizontal: 24, + }, + actionContainer: { + flex: 0, + paddingVertical: 16, + justifyContent: 'center', + }, + description: { + textAlign: 'center', + paddingBottom: 16, + }, + snapCell: { + marginVertical: 16, + }, + snapPermissionContainer: { + maxHeight: 300, + borderWidth: 1, + borderRadius: 8, + borderColor: colors.border.muted, + }, + iconContainer: { + justifyContent: 'center', + alignItems: 'center', + }, + iconWrapper: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: colors.success.muted, + justifyContent: 'center', + alignItems: 'center', + }, + }); +}; + +export default styleSheet; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/InstallSnapError.tsx b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/InstallSnapError.tsx new file mode 100644 index 00000000000..4f8f01eb6ba --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/InstallSnapError.tsx @@ -0,0 +1,100 @@ +import React, { useMemo } from 'react'; +import { View } from 'react-native'; +import stylesheet from './InstallSnapError.styles'; +import { strings } from '../../../../../../locales/i18n'; +import { + SNAP_INSTALL_ERROR, + SNAP_INSTALL_OK, +} from '../../../../../constants/test-ids'; +import SheetHeader from '../../../../../component-library/components/Sheet/SheetHeader'; +import Text, { + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import Icon, { + IconColor, + IconName, + IconSize, +} from '../../../../../component-library/components/Icons/Icon'; +import Cell, { + CellVariants, +} from '../../../../../component-library/components/Cells/Cell'; +import { AvatarVariants } from '../../../../../component-library/components/Avatars/Avatar'; +import { + ButtonSize, + ButtonVariants, +} from '../../../../../component-library/components/Buttons/Button'; +import BottomSheetFooter, { + ButtonsAlignment, +} from '../../../../../component-library/components/BottomSheets/BottomSheetFooter'; +import { ButtonProps } from '../../../../../component-library/components/Buttons/Button/Button.types'; +import { useStyles } from '../../../../hooks/useStyles'; +import { InstallSnapFlowProps } from '../../InstallSnapApprovalFlow.types'; + +const InstallSnapError = ({ + requestData, + onConfirm, + error, +}: InstallSnapFlowProps) => { + const { styles } = useStyles(stylesheet, {}); + + const snapName = useMemo(() => { + const colonIndex = requestData.snapId.indexOf(':'); + if (colonIndex !== -1) { + return requestData.snapId.substring(colonIndex + 1); + } + return requestData.snapId; + }, [requestData.snapId]); + + const okButtonProps: ButtonProps = { + variant: ButtonVariants.Primary, + label: strings('install_snap.okay_action'), + size: ButtonSize.Lg, + onPress: onConfirm, + testID: SNAP_INSTALL_OK, + }; + + const errorTitle = useMemo( + () => + error?.message ? error?.message : strings('install_snap.error_title'), + [error], + ); + + return ( + + + + + + + + + + + {strings('install_snap.error_description', { + snap: snapName, + })} + + + + + + + ); +}; + +export default InstallSnapError; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/index.ts b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/index.ts new file mode 100644 index 00000000000..bb74e27bb3e --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/index.ts @@ -0,0 +1,4 @@ +/* eslint-disable import/prefer-default-export */ +import InstallSnapError from './InstallSnapError'; + +export { InstallSnapError }; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/test/InstallSnapError.test.tsx b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/test/InstallSnapError.test.tsx new file mode 100644 index 00000000000..313275c7668 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapError/test/InstallSnapError.test.tsx @@ -0,0 +1,94 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import InstallSnapError from '../InstallSnapError'; +import { + SNAP_INSTALL_ERROR, + SNAP_INSTALL_OK, +} from '../../../../../../constants/test-ids'; + +describe('InstallSnapError', () => { + const requestData = { + metadata: { + id: 'uNadWHqPnwOM4NER3mERI', + origin: 'npm:@lavamoat/tss-snap', + dappOrigin: 'tss.ac', + }, + permissions: { + snap_manageState: {}, + 'endowment:rpc': { + caveats: [ + { + type: 'rpcOrigin', + value: { + dapps: true, + snaps: true, + }, + }, + ], + }, + }, + snapId: 'npm:@lavamoat/tss-snap', + }; + + const onConfirm = jest.fn(); + const onCancel = jest.fn(); + const error = new Error('Installation failed'); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(SNAP_INSTALL_ERROR)).toBeTruthy(); + }); + + it('calls onConfirm when the OK button is pressed', () => { + const { getByTestId } = render( + , + ); + + fireEvent.press(getByTestId(SNAP_INSTALL_OK)); + expect(onConfirm).toHaveBeenCalledTimes(1); + }); + + it('displays the correct snap name', () => { + const { getByText } = render( + , + ); + + const expectedSnapName = '@lavamoat/tss-snap'; + expect(getByText(expectedSnapName)).toBeTruthy(); + }); + + it('displays the correct error title', () => { + const { getByText } = render( + , + ); + + expect(getByText('Installation failed')).toBeTruthy(); + }); +}); diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/InstallSnapPermissionRequest.styles.ts b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/InstallSnapPermissionRequest.styles.ts new file mode 100644 index 00000000000..dcbcc3054db --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/InstallSnapPermissionRequest.styles.ts @@ -0,0 +1,48 @@ +import { StyleSheet } from 'react-native'; +import { Theme } from '../../../../../util/theme/models'; +import Device from '../../../../../util/device'; + +/** + * + * @param params Style sheet params. + * @param params.theme App theme from ThemeContext. + * @param params.vars Inputs that the style sheet depends on. + * @returns StyleSheet object. + */ +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + const { colors } = theme; + return StyleSheet.create({ + root: { + backgroundColor: colors.background.default, + paddingTop: 24, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + minHeight: 200, + paddingBottom: Device.isIphoneX() ? 20 : 0, + }, + accountCardWrapper: { + paddingHorizontal: 24, + }, + actionContainer: { + flex: 0, + paddingVertical: 16, + justifyContent: 'center', + }, + description: { + textAlign: 'center', + paddingBottom: 16, + }, + snapCell: { + marginVertical: 16, + }, + snapPermissionContainer: { + maxHeight: 300, + borderWidth: 1, + borderRadius: 8, + borderColor: colors.border.muted, + }, + }); +}; + +export default styleSheet; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/InstallSnapPermissionsRequest.tsx b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/InstallSnapPermissionsRequest.tsx new file mode 100644 index 00000000000..d6014c9ff9f --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/InstallSnapPermissionsRequest.tsx @@ -0,0 +1,104 @@ +import React, { useMemo } from 'react'; +import { ScrollView, View } from 'react-native'; +import stylesheet from './InstallSnapPermissionRequest.styles'; +import { strings } from '../../../../../../locales/i18n'; +import { + SNAP_INSTALL_CANCEL, + SNAP_INSTALL_PERMISSIONS_REQUEST, + SNAP_INSTALL_PERMISSIONS_REQUEST_APPROVE, +} from '../../../../../constants/test-ids'; +import SheetHeader from '../../../../../component-library/components/Sheet/SheetHeader'; +import Text, { + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import { IconName } from '../../../../../component-library/components/Icons/Icon'; +import Cell, { + CellVariants, +} from '../../../../../component-library/components/Cells/Cell'; +import { AvatarVariants } from '../../../../../component-library/components/Avatars/Avatar'; +import { + ButtonSize, + ButtonVariants, +} from '../../../../../component-library/components/Buttons/Button'; +import BottomSheetFooter, { + ButtonsAlignment, +} from '../../../../../component-library/components/BottomSheets/BottomSheetFooter'; +import { ButtonProps } from '../../../../../component-library/components/Buttons/Button/Button.types'; +import { useStyles } from '../../../../hooks/useStyles'; +import { InstallSnapFlowProps } from '../../InstallSnapApprovalFlow.types'; +import { SnapPermissions } from '../../../../Views/Snaps/components/SnapPermissions'; + +const InstallSnapPermissionsRequest = ({ + requestData, + onConfirm, + onCancel, +}: InstallSnapFlowProps) => { + const { styles } = useStyles(stylesheet, {}); + const snapName = useMemo(() => { + const colonIndex = requestData.snapId.indexOf(':'); + if (colonIndex !== -1) { + return requestData.snapId.substring(colonIndex + 1); + } + return requestData.snapId; + }, [requestData.snapId]); + + const dappOrigin = useMemo( + () => requestData.metadata.dappOrigin, + [requestData.metadata.dappOrigin], + ); + + const cancelButtonProps: ButtonProps = { + variant: ButtonVariants.Secondary, + label: strings('accountApproval.cancel'), + size: ButtonSize.Lg, + onPress: onCancel, + testID: SNAP_INSTALL_CANCEL, + }; + + const connectButtonProps: ButtonProps = { + variant: ButtonVariants.Primary, + label: strings('install_snap.approve_permissions'), + size: ButtonSize.Lg, + onPress: onConfirm, + testID: SNAP_INSTALL_PERMISSIONS_REQUEST_APPROVE, + }; + + return ( + + + + + + {strings('install_snap.permissions_request_description', { + origin: dappOrigin, + snap: snapName, + })} + + + + + + + + + + ); +}; + +export default InstallSnapPermissionsRequest; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/index.ts b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/index.ts new file mode 100644 index 00000000000..a61745f8de8 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/index.ts @@ -0,0 +1,4 @@ +/* eslint-disable import/prefer-default-export */ +import InstallSnapPermissionsRequest from './InstallSnapPermissionsRequest'; + +export { InstallSnapPermissionsRequest }; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/test/InstallSnapPermissionsRequest.test.tsx b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/test/InstallSnapPermissionsRequest.test.tsx new file mode 100644 index 00000000000..7671bad8e4e --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapPermissionsRequest/test/InstallSnapPermissionsRequest.test.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { render } from '@testing-library/react-native'; +import InstallSnapPermissionsRequest from '../InstallSnapPermissionsRequest'; +import { SNAP_PERMISSION_CELL } from '../../../../../../constants/test-ids'; + +describe('InstallSnapPermissionsRequest', () => { + const requestData = { + metadata: { + id: 'uNadWHqPnwOM4NER3mERI', + origin: 'npm:@lavamoat/tss-snap', + dappOrigin: 'tss.ac', + }, + permissions: { + snap_manageState: {}, + 'endowment:rpc': { + caveats: [ + { + type: 'rpcOrigin', + value: { + dapps: true, + snaps: true, + }, + }, + ], + }, + }, + snapId: 'npm:@lavamoat/tss-snap', + }; + + const onConfirm = jest.fn(); + const onCancel = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders the correct number of permission cells', () => { + const { getAllByTestId } = render( + , + ); + const permissionCells = getAllByTestId(SNAP_PERMISSION_CELL); + expect(permissionCells).toHaveLength(3); + }); +}); diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/InstallSnapSuccess.styles.ts b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/InstallSnapSuccess.styles.ts new file mode 100644 index 00000000000..a1a77544a57 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/InstallSnapSuccess.styles.ts @@ -0,0 +1,60 @@ +import { StyleSheet } from 'react-native'; +import { Theme } from '../../../../../util/theme/models'; +import Device from '../../../../../util/device'; + +/** + * + * @param params Style sheet params. + * @param params.theme App theme from ThemeContext. + * @param params.vars Inputs that the style sheet depends on. + * @returns StyleSheet object. + */ +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + const { colors } = theme; + return StyleSheet.create({ + root: { + backgroundColor: colors.background.default, + paddingTop: 24, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + minHeight: 200, + paddingBottom: Device.isIphoneX() ? 20 : 0, + }, + accountCardWrapper: { + paddingHorizontal: 24, + }, + actionContainer: { + flex: 0, + paddingVertical: 16, + justifyContent: 'center', + }, + description: { + textAlign: 'center', + paddingBottom: 16, + }, + snapCell: { + marginVertical: 16, + }, + snapPermissionContainer: { + maxHeight: 300, + borderWidth: 1, + borderRadius: 8, + borderColor: colors.border.muted, + }, + iconContainer: { + justifyContent: 'center', + alignItems: 'center', + }, + iconWrapper: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: colors.success.muted, + justifyContent: 'center', + alignItems: 'center', + }, + }); +}; + +export default styleSheet; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/InstallSnapSuccess.tsx b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/InstallSnapSuccess.tsx new file mode 100644 index 00000000000..2a3dcd772b2 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/InstallSnapSuccess.tsx @@ -0,0 +1,93 @@ +import React, { useMemo } from 'react'; +import { View } from 'react-native'; +import stylesheet from './InstallSnapSuccess.styles'; +import { strings } from '../../../../../../locales/i18n'; +import { + SNAP_INSTALL_OK, + SNAP_INSTALL_SUCCESS, +} from '../../../../../constants/test-ids'; +import SheetHeader from '../../../../../component-library/components/Sheet/SheetHeader'; +import Text, { + TextVariant, +} from '../../../../../component-library/components/Texts/Text'; +import Icon, { + IconColor, + IconName, + IconSize, +} from '../../../../../component-library/components/Icons/Icon'; +import Cell, { + CellVariants, +} from '../../../../../component-library/components/Cells/Cell'; +import { AvatarVariants } from '../../../../../component-library/components/Avatars/Avatar'; +import { + ButtonSize, + ButtonVariants, +} from '../../../../../component-library/components/Buttons/Button'; +import BottomSheetFooter, { + ButtonsAlignment, +} from '../../../../../component-library/components/BottomSheets/BottomSheetFooter'; +import { ButtonProps } from '../../../../../component-library/components/Buttons/Button/Button.types'; +import { useStyles } from '../../../../hooks/useStyles'; +import { InstallSnapFlowProps } from '../../InstallSnapApprovalFlow.types'; + +const InstallSnapSuccess = ({ + requestData, + onConfirm, +}: InstallSnapFlowProps) => { + const { styles } = useStyles(stylesheet, {}); + + const snapName = useMemo(() => { + const colonIndex = requestData.snapId.indexOf(':'); + if (colonIndex !== -1) { + return requestData.snapId.substring(colonIndex + 1); + } + return requestData.snapId; + }, [requestData.snapId]); + + const okButtonProps: ButtonProps = { + variant: ButtonVariants.Primary, + label: strings('install_snap.okay_action'), + size: ButtonSize.Lg, + onPress: onConfirm, + testID: SNAP_INSTALL_OK, + }; + + return ( + + + + + + + + + + + {strings('install_snap.install_successful', { + snap: snapName, + })} + + + + + + + ); +}; + +export default InstallSnapSuccess; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/index.ts b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/index.ts new file mode 100644 index 00000000000..a8bf2beafdd --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/index.ts @@ -0,0 +1,4 @@ +/* eslint-disable import/prefer-default-export */ +import InstallSnapSuccess from './InstallSnapSuccess'; + +export { InstallSnapSuccess }; diff --git a/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/test/InstallSnapSuccess.test.tsx b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/test/InstallSnapSuccess.test.tsx new file mode 100644 index 00000000000..5ef37ca9584 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/components/InstallSnapSuccess/test/InstallSnapSuccess.test.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react-native'; +import InstallSnapSuccess from '../InstallSnapSuccess'; +import { + SNAP_INSTALL_OK, + SNAP_INSTALL_SUCCESS, +} from '../../../../../../constants/test-ids'; + +describe('InstallSnapSuccess', () => { + const requestData = { + metadata: { + id: 'uNadWHqPnwOM4NER3mERI', + origin: 'npm:@lavamoat/tss-snap', + dappOrigin: 'tss.ac', + }, + permissions: { + snap_manageState: {}, + 'endowment:rpc': { + caveats: [ + { + type: 'rpcOrigin', + value: { + dapps: true, + snaps: true, + }, + }, + ], + }, + }, + snapId: 'npm:@lavamoat/tss-snap', + }; + + const onConfirm = jest.fn(); + const onCancel = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders correctly', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(SNAP_INSTALL_SUCCESS)).toBeDefined(); + }); + + it('calls onConfirm when the OK button is pressed', () => { + const { getByTestId } = render( + , + ); + + fireEvent.press(getByTestId(SNAP_INSTALL_OK)); + expect(onConfirm).toHaveBeenCalledTimes(1); + }); + + it('displays the correct snap name', () => { + const { getByText } = render( + , + ); + + const expectedSnapName = '@lavamoat/tss-snap'; + expect(getByText(expectedSnapName)).toBeTruthy(); + }); +}); diff --git a/app/components/UI/InstallSnapApprovalFlow/index.ts b/app/components/UI/InstallSnapApprovalFlow/index.ts new file mode 100644 index 00000000000..c7d294ce6f7 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/index.ts @@ -0,0 +1,5 @@ +/* eslint-disable import/prefer-default-export */ +import { InstallSnapApprovalArgs } from './InstallSnapApprovalFlow.types'; +import InstallSnapApprovalFlow from './InstallSnapApprovalFlow'; +export { InstallSnapApprovalFlow }; +export type { InstallSnapApprovalArgs }; diff --git a/app/components/UI/InstallSnapApprovalFlow/test/InstallSnapApprovalFlow.test.tsx b/app/components/UI/InstallSnapApprovalFlow/test/InstallSnapApprovalFlow.test.tsx new file mode 100644 index 00000000000..fa553ea9fc2 --- /dev/null +++ b/app/components/UI/InstallSnapApprovalFlow/test/InstallSnapApprovalFlow.test.tsx @@ -0,0 +1,159 @@ +import React from 'react'; +import { render, fireEvent, waitFor } from '@testing-library/react-native'; +import InstallSnapApprovalFlow from '../InstallSnapApprovalFlow'; +import { + SNAP_INSTALL_CANCEL, + SNAP_INSTALL_CONNECT, + SNAP_INSTALL_CONNECTION_REQUEST, + SNAP_INSTALL_ERROR, + SNAP_INSTALL_PERMISSIONS_REQUEST, + SNAP_INSTALL_PERMISSIONS_REQUEST_APPROVE, + SNAP_INSTALL_SUCCESS, +} from '../../../../constants/test-ids'; + +describe('InstallSnapApprovalFlow', () => { + const requestData = { + requestData: { + metadata: { + id: 'uNadWHqPnwOM4NER3mERI', + origin: 'npm:@lavamoat/tss-snap', + dappOrigin: 'tss.ac', + }, + permissions: { + snap_manageState: {}, + 'endowment:rpc': { + caveats: [ + { + type: 'rpcOrigin', + value: { + dapps: true, + snaps: false, + }, + }, + ], + }, + }, + snapId: 'npm:@lavamoat/tss-snap', + }, + id: 'uNadWHqPnwOM4NER3mERI', + }; + + const onConfirm = jest.fn(); + const onFinish = jest.fn(); + const onCancel = jest.fn(); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('renders InstallSnapConnectionRequest component initially', () => { + const { getByTestId } = render( + , + ); + const connectionRequest = getByTestId(SNAP_INSTALL_CONNECTION_REQUEST); + expect(connectionRequest).toBeDefined(); + }); + + it('switches to InstallSnapPermissionsRequest on confirmation', async () => { + const { getByTestId, findByTestId } = render( + , + ); + const confirmButton = getByTestId(SNAP_INSTALL_CONNECT); + fireEvent.press(confirmButton); + const permissionsRequest = await findByTestId( + SNAP_INSTALL_PERMISSIONS_REQUEST, + ); + expect(permissionsRequest).toBeDefined(); + }); + + it('calls onConfirm when Approve button is pressed in InstallSnapPermissionsRequest', async () => { + const { getByTestId } = render( + , + ); + const confirmButton = getByTestId(SNAP_INSTALL_CONNECT); + fireEvent.press(confirmButton); + const permissionsApproveButton = getByTestId( + SNAP_INSTALL_PERMISSIONS_REQUEST_APPROVE, + ); + fireEvent.press(permissionsApproveButton); + expect(onConfirm).toHaveBeenCalledTimes(1); + }); + + it('renders InstallSnapSuccess on successful installation', async () => { + const { getByTestId, findByTestId } = render( + , + ); + const confirmButton = getByTestId(SNAP_INSTALL_CONNECT); + fireEvent.press(confirmButton); + const permissionsRequest = await findByTestId( + SNAP_INSTALL_PERMISSIONS_REQUEST, + ); + expect(permissionsRequest).toBeDefined(); + const permissionsApproveButton = getByTestId( + SNAP_INSTALL_PERMISSIONS_REQUEST_APPROVE, + ); + fireEvent.press(permissionsApproveButton); + const installSuccess = await findByTestId(SNAP_INSTALL_SUCCESS); + expect(installSuccess).toBeDefined(); + }); + + it('renders InstallSnapError on error during installation', async () => { + onConfirm.mockImplementation(() => { + throw new Error('Installation error'); + }); + + const { getByTestId, findByTestId } = render( + , + ); + const confirmButton = getByTestId(SNAP_INSTALL_CONNECT); + fireEvent.press(confirmButton); + const permissionsRequest = getByTestId(SNAP_INSTALL_PERMISSIONS_REQUEST); + expect(permissionsRequest).toBeDefined(); + const permissionsConfirmButton = getByTestId( + SNAP_INSTALL_PERMISSIONS_REQUEST_APPROVE, + ); + fireEvent.press(permissionsConfirmButton); + await waitFor(() => { + expect(findByTestId(SNAP_INSTALL_ERROR)).toBeDefined(); + }); + }); + + it('calls onCancel on cancel button click', () => { + const { getByTestId } = render( + , + ); + const cancelButton = getByTestId(SNAP_INSTALL_CANCEL); + fireEvent.press(cancelButton); + expect(onCancel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app/components/UI/SnapsExecutionWebView/SnapsExecutionWebView.tsx b/app/components/UI/SnapsExecutionWebView/SnapsExecutionWebView.tsx new file mode 100644 index 00000000000..e6b5d62745e --- /dev/null +++ b/app/components/UI/SnapsExecutionWebView/SnapsExecutionWebView.tsx @@ -0,0 +1,62 @@ +import React, { useRef } from 'react'; +import { View, ScrollView } from 'react-native'; +import WebView from 'react-native-webview'; +import { snapsState, WebviewPostMessageStream } from '../../../core/Snaps'; +import { createStyles } from './styles'; + +let stream: any; + +const SnapsExecutionWebView = () => { + const styles = createStyles(); + + const webviewRef = useRef(); + + const setWebviewPostMessage = () => { + stream = new WebviewPostMessageStream({ + name: 'rnside', + target: 'webview', + targetOrigin: '*', + targetWindow: webviewRef.current, + }); + + // eslint-disable-next-line no-console + stream.on('data', (data: any) => + // eslint-disable-next-line no-console + console.log( + '[APP LOG] setWebviewPostMessage: Message from Webview ' + data, + ), + ); + + snapsState.stream = stream; + snapsState.webview = webviewRef.current; + }; + + const messageFromWebview = (data: any) => { + stream?._onMessage(data); + }; + + const envURI = { + prod: 'https://gantunesr.github.io/mobile-execution-environment/', + localIOS: 'http://localhost:3001/mobile-execution-environment', + localAndroid: 'http://10.0.2.2:3001/mobile-execution-environment', + }; + + return ( + + + + + + ); +}; + +export default SnapsExecutionWebView; diff --git a/app/components/UI/SnapsExecutionWebView/index.ts b/app/components/UI/SnapsExecutionWebView/index.ts new file mode 100644 index 00000000000..5e8c0f374a9 --- /dev/null +++ b/app/components/UI/SnapsExecutionWebView/index.ts @@ -0,0 +1,4 @@ +import SnapsExecutionWebView from './SnapsExecutionWebView'; + +// eslint-disable-next-line import/prefer-default-export +export { SnapsExecutionWebView }; diff --git a/app/components/UI/SnapsExecutionWebView/styles.ts b/app/components/UI/SnapsExecutionWebView/styles.ts new file mode 100644 index 00000000000..659b2428b60 --- /dev/null +++ b/app/components/UI/SnapsExecutionWebView/styles.ts @@ -0,0 +1,20 @@ +/* eslint-disable react-native/no-color-literals */ +import { StyleSheet } from 'react-native'; + +// eslint-disable-next-line import/prefer-default-export +export const createStyles = () => + StyleSheet.create({ + webview: { + height: 0, + // marginBottom: 50, + // borderWidth: 1, + // borderStyle: 'dashed', + // borderColor: 'red', + }, + container: { + // flex: 1, + // borderWidth: 1, + // borderStyle: 'dashed', + // borderColor: 'green', + }, + }); diff --git a/app/components/Views/AccountConnect/AccountConnect.tsx b/app/components/Views/AccountConnect/AccountConnect.tsx index a027349b2bf..47a20278fe9 100644 --- a/app/components/Views/AccountConnect/AccountConnect.tsx +++ b/app/components/Views/AccountConnect/AccountConnect.tsx @@ -35,7 +35,7 @@ import { getUrlObj, prefixUrlWithProtocol } from '../../../util/browser'; import { strings } from '../../../../locales/i18n'; import { AvatarAccountType } from '../../../component-library/components/Avatars/Avatar/variants/AvatarAccount'; import { safeToChecksumAddress } from '../../../util/address'; -import USER_INTENT from '../../../constants/permissions'; +import { USER_INTENT } from '../../../constants/permissions'; // Internal dependencies. import { diff --git a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx index e9cb31f0070..d7def3fcb64 100644 --- a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx +++ b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.tsx @@ -14,7 +14,7 @@ import Button, { ButtonWidthTypes, } from '../../../../component-library/components/Buttons/Button'; import AccountSelectorList from '../../../UI/AccountSelectorList'; -import USER_INTENT from '../../../../constants/permissions'; +import { USER_INTENT } from '../../../../constants/permissions'; import generateTestId from '../../../../../wdio/utils/generateTestId'; import { ACCOUNT_APPROVAL_SELECT_ALL_BUTTON } from '../../../../../wdio/screen-objects/testIDs/Components/AccountApprovalModal.testIds'; diff --git a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.types.ts b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.types.ts index 7a4420f58c8..e0db8a7c2c4 100644 --- a/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.types.ts +++ b/app/components/Views/AccountConnect/AccountConnectMultiSelector/AccountConnectMultiSelector.types.ts @@ -12,7 +12,7 @@ export enum AccountConnectMultiSelectorScreens { // External dependencies. import { UseAccounts } from '../../../hooks/useAccounts'; import { IconName } from '../../../../component-library/components/Icons/Icon'; -import USER_INTENT from '../../../../constants/permissions'; +import { USER_INTENT } from '../../../../constants/permissions'; /** * AccountConnectMultiSelector props. diff --git a/app/components/Views/AccountConnect/AccountConnectSingle/AccountConnectSingle.tsx b/app/components/Views/AccountConnect/AccountConnectSingle/AccountConnectSingle.tsx index 0a864dc50ff..7ff499d6732 100644 --- a/app/components/Views/AccountConnect/AccountConnectSingle/AccountConnectSingle.tsx +++ b/app/components/Views/AccountConnect/AccountConnectSingle/AccountConnectSingle.tsx @@ -29,7 +29,7 @@ import { AccountConnectScreens } from '../AccountConnect.types'; // Internal dependencies. import { AccountConnectSingleProps } from './AccountConnectSingle.types'; import styleSheet from './AccountConnectSingle.styles'; -import USER_INTENT from '../../../../constants/permissions'; +import { USER_INTENT } from '../../../../constants/permissions'; import { ACCOUNT_APROVAL_MODAL_CONTAINER_ID, diff --git a/app/components/Views/AccountConnect/AccountConnectSingle/AccountConnectSingle.types.ts b/app/components/Views/AccountConnect/AccountConnectSingle/AccountConnectSingle.types.ts index 702a2928bea..197398910ec 100644 --- a/app/components/Views/AccountConnect/AccountConnectSingle/AccountConnectSingle.types.ts +++ b/app/components/Views/AccountConnect/AccountConnectSingle/AccountConnectSingle.types.ts @@ -5,7 +5,7 @@ import { ImageSourcePropType } from 'react-native'; import { AccountConnectScreens } from '../AccountConnect.types'; import { Account } from '../../../hooks/useAccounts'; import { IconName } from '../../../../component-library/components/Icons/Icon'; -import USER_INTENT from '../../../../constants/permissions'; +import { USER_INTENT } from '../../../../constants/permissions'; /** * AccountConnectSingle props. diff --git a/app/components/Views/AccountConnect/AccountConnectSingleSelector/AccountConnectSingleSelector.tsx b/app/components/Views/AccountConnect/AccountConnectSingleSelector/AccountConnectSingleSelector.tsx index fdc06822918..152aef7761a 100644 --- a/app/components/Views/AccountConnect/AccountConnectSingleSelector/AccountConnectSingleSelector.tsx +++ b/app/components/Views/AccountConnect/AccountConnectSingleSelector/AccountConnectSingleSelector.tsx @@ -12,7 +12,7 @@ import { AccountConnectScreens } from '../AccountConnect.types'; // Internal dependencies. import { AccountConnectSingleSelectorProps } from './AccountConnectSingleSelector.types'; import styles from './AccountConnectSingleSelector.styles'; -import USER_INTENT from '../../../../constants/permissions'; +import { USER_INTENT } from '../../../../constants/permissions'; const AccountConnectSingleSelector = ({ accounts, diff --git a/app/components/Views/AccountConnect/AccountConnectSingleSelector/AccountConnectSingleSelector.types.ts b/app/components/Views/AccountConnect/AccountConnectSingleSelector/AccountConnectSingleSelector.types.ts index a07edeb7466..839bbf19eeb 100644 --- a/app/components/Views/AccountConnect/AccountConnectSingleSelector/AccountConnectSingleSelector.types.ts +++ b/app/components/Views/AccountConnect/AccountConnectSingleSelector/AccountConnectSingleSelector.types.ts @@ -1,6 +1,6 @@ // External dependencies. import { UseAccounts } from '../../../hooks/useAccounts'; -import USER_INTENT from '../../../../constants/permissions'; +import { USER_INTENT } from '../../../../constants/permissions'; import { AccountConnectScreens } from '../AccountConnect.types'; /** diff --git a/app/components/Views/AccountPermissions/AccountPermissions.tsx b/app/components/Views/AccountPermissions/AccountPermissions.tsx index b220895e4b3..761e0ca6567 100755 --- a/app/components/Views/AccountPermissions/AccountPermissions.tsx +++ b/app/components/Views/AccountPermissions/AccountPermissions.tsx @@ -45,7 +45,7 @@ import { } from './AccountPermissions.types'; import AccountPermissionsConnected from './AccountPermissionsConnected'; import AccountPermissionsRevoke from './AccountPermissionsRevoke'; -import USER_INTENT from '../../../constants/permissions'; +import { USER_INTENT } from '../../../constants/permissions'; const AccountPermissions = (props: AccountPermissionsProps) => { const navigation = useNavigation(); diff --git a/app/components/Views/Settings/index.js b/app/components/Views/Settings/index.js index 846402c9512..f07187f4b45 100644 --- a/app/components/Views/Settings/index.js +++ b/app/components/Views/Settings/index.js @@ -9,6 +9,7 @@ import { MetaMetricsEvents } from '../../../core/Analytics'; import { connect } from 'react-redux'; import { ThemeContext, mockTheme } from '../../../util/theme'; import Routes from '../../../constants/navigation/Routes'; +import { createSnapsSettingsListNavDetails } from '../Snaps/SnapsSettingsList/SnapsSettingsList'; const createStyles = (colors) => StyleSheet.create({ @@ -102,6 +103,10 @@ class Settings extends PureComponent { this.props.navigation.navigate('ContactsSettings'); }; + onPressSnaps = () => { + this.props.navigation.navigate(...createSnapsSettingsListNavDetails()); + }; + render = () => { const { seedphraseBackedUp } = this.props; const colors = this.context.colors || mockTheme.colors; @@ -135,6 +140,11 @@ class Settings extends PureComponent { description={strings('app_settings.networks_desc')} onPress={this.onPressNetworks} /> + + StyleSheet.create({ + snapSettingsContainer: { + flex: 1, + marginHorizontal: 16, + }, + itemPaddedContainer: { + paddingVertical: 16, + }, + removeSection: { + paddingTop: 32, + }, + removeButton: { + marginVertical: 16, + }, + }); + +export default styleSheet; diff --git a/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx b/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx new file mode 100644 index 00000000000..66db6285e96 --- /dev/null +++ b/app/components/Views/Snaps/SnapSettings/SnapSettings.tsx @@ -0,0 +1,118 @@ +import React, { useCallback, useEffect } from 'react'; +import { View, ScrollView, SafeAreaView } from 'react-native'; + +import Engine from '../../../../core/Engine'; +import Text, { + TextVariant, +} from '../../../../component-library/components/Texts/Text'; +import Button, { + ButtonVariants, + ButtonWidthTypes, +} from '../../../../component-library/components/Buttons/Button'; + +import stylesheet from './SnapSettings.styles'; +import { + createNavigationDetails, + useParams, +} from '../../../../util/navigation/navUtils'; +import Routes from '../../../../constants/navigation/Routes'; +import { Snap } from '@metamask/snaps-utils'; +import { getNavigationOptionsTitle } from '../../../UI/Navbar'; +import { useNavigation } from '@react-navigation/native'; +import { SnapDetails } from '../components/SnapDetails'; +import { SnapDescription } from '../components/SnapDescription'; +import { SnapPermissions } from '../components/SnapPermissions'; +import { SNAP_SETTINGS_REMOVE_BUTTON } from '../../../../constants/test-ids'; +import { strings } from '../../../../../locales/i18n'; +import { useStyles } from '../../../hooks/useStyles'; +import { useSelector } from 'react-redux'; + +interface SnapSettingsProps { + snap: Snap; +} + +export const createSnapSettingsNavDetails = + createNavigationDetails(Routes.SNAPS.SNAP_SETTINGS); + +const SnapSettings = () => { + const { styles, theme } = useStyles(stylesheet, {}); + const { colors } = theme; + const navigation = useNavigation(); + + const { snap } = useParams(); + + const permissionsState = useSelector( + (state: any) => state.engine.backgroundState.PermissionController, + ); + + function getPermissionSubjects(state: any) { + return state.subjects || {}; + } + + function getPermissions(state: any, origin: any) { + return getPermissionSubjects(state)[origin]?.permissions; + } + + const permissionsFromController = getPermissions(permissionsState, snap.id); + + useEffect(() => { + navigation.setOptions( + getNavigationOptionsTitle( + `${snap.manifest.proposedName}`, + navigation, + false, + colors, + false, + ), + ); + }, [colors, navigation, snap.manifest.proposedName]); + + const removeSnap = useCallback(async () => { + const { SnapController } = Engine.context as any; + await SnapController.removeSnap(snap.id); + navigation.goBack(); + }, [navigation, snap.id]); + + return ( + + + + + + + + + + + + {strings( + 'app_settings.snaps.snap_settings.remove_snap_section_title', + )} + + + {strings( + 'app_settings.snaps.snap_settings.remove_snap_section_description', + )} + +