Skip to content

Commit

Permalink
Merge branch 'main' into chore/upgrade-signature-controller-remove-gl…
Browse files Browse the repository at this point in the history
…obal-network
  • Loading branch information
matthewwalsh0 committed Oct 29, 2024
2 parents 4e2f48c + afb8576 commit b544f18
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 94 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,25 @@ cd metamask-mobile

**Firebase Messaging Setup**

Before running the app, keep in mind that MetaMask uses FCM (Firebase Cloud Message) to empower communications. Based on this, as an external contributor you would preferably need to provide your own FREE Firebase project config file with a matching client for package name `io.metamask`, and update your `google-services.json` file in the `android/app` or `GoogleService-Info.plist` file in the `ios` directory. In case you don't have FCM account, you can use `./android/app/google-services-example.json` for Android or `./ios/GoogleServices/GoogleService-Info-example.plist` for iOS and follow the steps below to populate the correct environment variables in the `.env` files (`.ios.env`, `.js.env`, `.android.env`), adding `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` variable depending on the environment you are running the app (ios/android).
Before running the app, keep in mind that MetaMask uses FCM (Firebase Cloud Message) to empower communications. Based on this, as an external contributor you would preferably need to provide your own FREE Firebase project config file with a matching client for package name `io.metamask`, and update your `google-services.json` file in the `android/app` or `GoogleService-Info.plist` file in the `ios` directory.

**External Contributors**
In case you don't have FCM account, you can use `./android/app/google-services-example.json` for Android or `./ios/GoogleServices/GoogleService-Info-example.plist` for iOS and follow the steps below to populate the correct environment variables in the `.env` files (`.ios.env`, `.js.env`, `.android.env`), adding `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` variable depending on the environment you are running the app (ios/android).

**Internal Contributors**

We should access the Firebase project config file from 1Password.

The value you should provide to `GOOGLE_SERVICES_B64_ANDROID` or `GOOGLE_SERVICES_B64_IOS` is the base64 encoded version of your Firebase project config file, which can be generated as follows:

**For Android**
```bash
echo "export GOOGLE_SERVICES_B64_ANDROID=\"$(base64 -w0 -i ./android/app/google-services-example.json)\"" | tee -a .js.env .ios.env .android.env
export GOOGLE_SERVICES_B64_ANDROID="$(base64 -w0 -i ./android/app/google-services-example.json)" && echo "export GOOGLE_SERVICES_B64_ANDROID=\"$GOOGLE_SERVICES_B64_ANDROID\"" | tee -a .js.env .ios.env
```

**For iOS**
```bash
echo "export GOOGLE_SERVICES_B64_IOS=\"$(base64 -w0 -i ./ios/GoogleServices/GoogleService-Info-example.plist)\"" | tee -a .js.env .ios.env
export GOOGLE_SERVICES_B64_IOS="$(base64 -w0 -i ./ios/GoogleServices/GoogleService-Info-example.plist)" && echo "export GOOGLE_SERVICES_B64_IOS=\"$GOOGLE_SERVICES_B64_IOS\"" | tee -a .js.env .ios.env
```

[!CAUTION]
Expand Down
45 changes: 45 additions & 0 deletions app/components/Views/LedgerConnect/Scan.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import useBluetoothDevices from '../../hooks/Ledger/useBluetoothDevices';
import useBluetoothPermissions from '../../hooks/useBluetoothPermissions';
import useBluetooth from '../../hooks/Ledger/useBluetooth';
import { BluetoothPermissionErrors } from '../../../core/Ledger/ledgerErrors';
import { fireEvent } from '@testing-library/react-native';
import { SELECT_DROP_DOWN } from '../../UI/SelectOptionSheet/constants';
import { NavigationProp, ParamListBase, useNavigation } from '@react-navigation/native';

jest.mock('../../hooks/Ledger/useBluetooth');
jest.mock('../../hooks/Ledger/useBluetoothDevices');
Expand All @@ -19,6 +22,11 @@ jest.mock('@react-navigation/native', () => ({
useNavigation: jest.fn(),
}));

jest.mock('@react-navigation/native', () => ({
...jest.requireActual('@react-navigation/native'),
useNavigation: jest.fn(),
}));

describe('Scan', () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down Expand Up @@ -175,4 +183,41 @@ describe('Scan', () => {

expect(onScanningErrorStateChanged).toHaveBeenCalled();
});

it('calls onValueChange when select different device', () => {
const device1 = {
id: 'device1',
name: 'Device 1',
};
const device2 = {
id: 'device2',
name: 'Device 2',
};

const onDeviceSelected = jest.fn();

const navigateMock = {
navigate: jest.fn().mockImplementation(() => {onDeviceSelected(device2)}),

Check warning on line 200 in app/components/Views/LedgerConnect/Scan.test.tsx

View workflow job for this annotation

GitHub Actions / scripts (lint)

Missing semicolon
} as unknown as NavigationProp<ParamListBase>;
jest.mocked(useNavigation).mockReturnValue(navigateMock);

jest.mocked(useBluetoothDevices).mockReturnValue({
devices: [device1, device2],
deviceScanError: false,
});

const {getByTestId} = renderWithProvider(
<Scan
onDeviceSelected={onDeviceSelected}
onScanningErrorStateChanged={jest.fn()}
ledgerError={undefined}
/>,
);
expect(onDeviceSelected).toHaveBeenCalledWith(device1);

fireEvent.press(getByTestId(SELECT_DROP_DOWN));

expect(onDeviceSelected).toHaveBeenCalledWith(device2);
});

});
8 changes: 4 additions & 4 deletions app/components/Views/LedgerConnect/Scan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,12 @@ const Scan = ({
]);

useEffect(() => {
// first device is selected by default
if (devices?.length > 0) {
// first device is selected by default if not selectedDevice is set
if (devices?.length > 0 && !selectedDevice) {
setSelectedDevice(devices[0]);
onDeviceSelected(devices[0]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [devices]);
}, [devices, onDeviceSelected, selectedDevice]);

useEffect(() => {
if (bluetoothPermissionError && !permissionErrorShown) {
Expand Down
61 changes: 61 additions & 0 deletions app/components/hooks/Ledger/useBluetoothDevices.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { renderHook } from '@testing-library/react-hooks';
import useBluetoothDevices from './useBluetoothDevices';
import { Observable } from 'rxjs';

describe('useBluetoothDevices', () => {
let subscribeMock: jest.Mock;

beforeEach(() => {
subscribeMock = jest.fn();
jest.spyOn(Observable.prototype, 'subscribe').mockImplementation(subscribeMock);
});

afterEach(() => {
jest.clearAllMocks();
});

it('returns empty devices and no error when permissions are false', () => {
const { result } = renderHook(() => useBluetoothDevices(false, true));

expect(result.current.devices).toEqual([]);
expect(result.current.deviceScanError).toBe(false);
});

it('returns empty devices and no error when bluetooth is off', () => {
const { result } = renderHook(() => useBluetoothDevices(true, false));

expect(result.current.devices).toEqual([]);
expect(result.current.deviceScanError).toBe(false);
});

it('returns expected device and no error when permissions and bluetooth are on and devices scan return devices', async () => {

const expectedDevice1 = {
id: '1',
name: 'Device 1',
};

// Mock `listen` to simulate device discovery
subscribeMock.mockImplementation(({ next }) => {
next({
type: 'add',
descriptor: expectedDevice1,
});
});
const { result } = renderHook(() => useBluetoothDevices(true, true));

expect(result.current.devices).toEqual([expectedDevice1]);
expect(result.current.deviceScanError).toBe(false);
});

it('returns error when device scan fails', async () => {
// Mock `listen` to simulate device discovery
subscribeMock.mockImplementation(({ error }) => {
error('Error');
});
const { result } = renderHook(() => useBluetoothDevices(true, true));

expect(result.current.devices).toEqual([]);
expect(result.current.deviceScanError).toBe(true);
});
});
55 changes: 30 additions & 25 deletions app/components/hooks/Ledger/useBluetoothDevices.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import TransportBLE from '@ledgerhq/react-native-hw-transport-ble';
import { Observable, Observer, Subscription } from 'rxjs';

export interface BluetoothDevice {
Expand All @@ -20,44 +21,33 @@ export interface BluetoothInterface {
close(): void;
}

export interface ObservableEventType {
type: string,
descriptor: BluetoothDevice,
deviceModel: never,
}

const useBluetoothDevices = (
hasBluetoothPermissions: boolean,
bluetoothOn: boolean,
) => {
const [devices, setDevices] = useState<Record<string, BluetoothDevice>>({});
const [deviceScanError, setDeviceScanError] = useState<boolean>(false);
const [observableEvent, setObservableEvent] = useState<ObservableEventType>();

// Initiate scanning and pairing if bluetooth is enabled
useEffect(() => {
let subscription: Subscription;

if (hasBluetoothPermissions && bluetoothOn) {
import('@ledgerhq/react-native-hw-transport-ble').then(
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(bluetoothInterface: any) => {
subscription = new Observable(
bluetoothInterface.default.listen,
).subscribe({
// TODO: Replace "any" with type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
next: (e: any) => {
const deviceFound = devices[e?.descriptor.id];

if (e.type === 'add' && !deviceFound) {
setDevices((prevValues) => ({
...prevValues,
[e.descriptor.id]: e.descriptor,
}));
setDeviceScanError(false);
}
},
error: (_error) => {
setDeviceScanError(true);
},
});
subscription = new Observable(TransportBLE.listen).subscribe({
next: (e: ObservableEventType) => {
setObservableEvent(e);
},
error: (_error) => {
setDeviceScanError(true);
},
);
});
}

return () => {
Expand All @@ -66,6 +56,21 @@ const useBluetoothDevices = (
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasBluetoothPermissions, bluetoothOn]);

useEffect(() =>{
if (observableEvent?.descriptor) {
const btDevice = observableEvent.descriptor;
const deviceFound = devices[btDevice.id];

if (observableEvent.type === 'add' && !deviceFound) {
setDevices((prevValues) => ({
...prevValues,
[btDevice.id]: btDevice,
}));
setDeviceScanError(false);
}
}
}, [observableEvent, devices]);

return {
deviceScanError,
devices: Object.values(devices),
Expand Down
2 changes: 1 addition & 1 deletion app/core/BackgroundBridge/BackgroundBridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { SubjectType } from '@metamask/permission-controller';

const createFilterMiddleware = require('@metamask/eth-json-rpc-filters');
const createSubscriptionManager = require('@metamask/eth-json-rpc-filters/subscriptionManager');
const { providerAsMiddleware } = require('eth-json-rpc-middleware');
const { providerAsMiddleware } = require('@metamask/eth-json-rpc-middleware');
const pump = require('pump');
// eslint-disable-next-line import/no-nodejs-modules
const EventEmitter = require('events').EventEmitter;
Expand Down
2 changes: 1 addition & 1 deletion app/core/Snaps/SnapBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { SubjectType } from '@metamask/permission-controller';
const ObjectMultiplex = require('@metamask/object-multiplex');
const createFilterMiddleware = require('@metamask/eth-json-rpc-filters');
const createSubscriptionManager = require('@metamask/eth-json-rpc-filters/subscriptionManager');
const { providerAsMiddleware } = require('eth-json-rpc-middleware');
const { providerAsMiddleware } = require('@metamask/eth-json-rpc-middleware');
const pump = require('pump');

interface ISnapBridgeProps {
Expand Down
2 changes: 1 addition & 1 deletion ios/GoogleServices/GoogleService-Info-example.plist
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,6 @@
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>mock-google-app-id-1234</string>
<string>1:824598429541:ios:7b7482c4598025a5beab8c</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
"@metamask/controller-utils": "^11.3.0",
"@metamask/design-tokens": "^4.0.0",
"@metamask/eth-json-rpc-filters": "^8.0.0",
"@metamask/eth-json-rpc-middleware": "^11.0.2",
"@metamask/eth-ledger-bridge-keyring": "^4.1.0",
"@metamask/eth-query": "^4.0.0",
"@metamask/eth-sig-util": "^7.0.2",
Expand Down Expand Up @@ -240,7 +241,6 @@
"eciesjs": "^0.3.15",
"eth-block-tracker": "^7.0.1",
"eth-ens-namehash": "2.0.8",
"eth-json-rpc-middleware": "9.0.1",
"eth-url-parser": "1.0.4",
"ethereumjs-abi": "0.6.6",
"ethereumjs-util": "6.1.0",
Expand Down
Loading

0 comments on commit b544f18

Please sign in to comment.