Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add useInternetConnection hook to track internet connectivity changes #665

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion packages/@magic-sdk/react-native-bare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ npm install --save @magic-sdk/react-native-bare
npm install --save react-native-device-info # Required Peer Dependency
npm install --save @react-native-community/async-storage # Required Peer Dependency
npm install --save react-native-safe-area-context # Required Peer Dependency
npm install --save @react-native-community/netinfo # Required Peer Dependency

# Via Yarn:
yarn add @magic-sdk/react-native-bare
⁠yarn add react-native-device-info # Required Peer Dependency
yarn add react-native-device-info # Required Peer Dependency
yarn add @react-native-community/async-storage # Required Peer Dependency
yarn add react-native-safe-area-context # Required Peer Dependency
yarn add @react-native-community/netinfo # Required Peer Dependency
```

## ⚑️ Quick Start
Expand Down Expand Up @@ -69,4 +71,44 @@ Please note that as of **v14.0.0** our React Native package offerings wrap the `
We have also added an optional `backgroundColor` prop to the `Relayer` to fix issues with `SafeAreaView` showing the background. By default, the background will be white. If you have changed the background color as part of your [custom branding setup](https://magic.link/docs/authentication/features/login-ui#configuration), make sure to pass your custom background color to `magic.Relayer`:
```tsx
<magic.Relayer backgroundColor="#0000FF"/>
```

## πŸ™ŒπŸΎ Troubleshooting

### Symlinking in Monorepo w/ Metro

For React Native projects living within a **monorepo** that run into the following `TypeError: Undefined is not an object` error:

<img width="299" alt="Screenshot 2022-11-23 at 12 19 19 PM" src="https://user-images.githubusercontent.com/13407884/203641477-ec2e472e-86dc-4a22-b54a-eb694001617e.png">

When attempting to import `Magic`, take note that the React Native metro bundler doesn’t work well with symlinks, which tend to be utilized by most package managers.

For this issue consider using Microsoft's [rnx-kit](https://microsoft.github.io/rnx-kit/docs/guides/bundling) suite of tools that include a plugin for metro that fixes this symlink related error.

### Handling internet connection problems

When the app has internet connectivity issues, the relayer might behave in ways you don't expect. That is why it is important to use [@react-native-community/netinfo](https://www.npmjs.com/package/@react-native-community/netinfo) to track the internet connection state of the device, and refresh your UI when the connection is re-established. For your convenience, we've also added a hook that uses this library behind the scenes:
Ariflo marked this conversation as resolved.
Show resolved Hide resolved

```tsx
import { useInternetConnection } from '@magic-sdk/react-native-expo';

const magic = new Magic('YOUR_API_KEY');

const connected = useInternetConnection()

useEffect(() => {
if (!connected) {
// Unomount this component and show your "You're offline" screen.
}
}, [connected])

export default function App() {
return <>
<SafeAreaProvider>
{/* Render the Magic iframe! */}
<magic.Relayer />
{...}
</SafeAreaProvider>
</>
}
```
2 changes: 1 addition & 1 deletion packages/@magic-sdk/react-native-bare/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
...baseJestConfig,
preset: 'react-native',
preset: '@testing-library/react-native',
transform: {
'^.+\\.(js|jsx)$': 'babel-jest',
'\\.(ts|tsx)$': 'ts-jest',
Expand Down
6 changes: 5 additions & 1 deletion packages/@magic-sdk/react-native-bare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,19 @@
"@babel/plugin-transform-flow-strip-types": "^7.14.5",
"@babel/runtime": "~7.10.4",
"@react-native-async-storage/async-storage": "^1.15.5",
"@react-native-community/netinfo": ">11.0.0",
"@testing-library/react-native": "^12.4.0",
"metro-react-native-babel-preset": "^0.66.2",
"react": "^16.13.1",
"react-native": "^0.62.2",
"react-native-device-info": "^10.3.0",
"react-native-safe-area-context": "^4.4.1",
"react-native-webview": "^12.4.0"
"react-native-webview": "^12.4.0",
"react-test-renderer": "^16.13.1"
},
"peerDependencies": {
"@react-native-async-storage/async-storage": ">=1.15.5",
"@react-native-community/netinfo": ">11.0.0",
"react": ">=16",
"react-native": ">=0.60",
"react-native-device-info": ">=10.3.0",
Expand Down
16 changes: 16 additions & 0 deletions packages/@magic-sdk/react-native-bare/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useState } from 'react';
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';

export const useInternetConnection = () => {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const handleConnectionChange = (connectionInfo: NetInfoState) => {
setIsConnected(!!connectionInfo.isConnected);
};

// Subscribe to connection changes and cleanup on unmount
return NetInfo.addEventListener(handleConnectionChange);
}, []);

return isConnected;
};
2 changes: 2 additions & 0 deletions packages/@magic-sdk/react-native-bare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ export type Magic<T extends MagicSDKExtensionsOption<any> = MagicSDKExtensionsOp
SDKBaseReactNative,
T
>;

export { useInternetConnection } from './hooks';
Ariflo marked this conversation as resolved.
Show resolved Hide resolved
40 changes: 39 additions & 1 deletion packages/@magic-sdk/react-native-bare/test/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
// @react-native-community/netinfo mocks
const defaultState = {
type: 'cellular',
isConnected: true,
isInternetReachable: true,
details: {
isConnectionExpensive: true,
cellularGeneration: '3g',
},
};

const NetInfoStateType = {
unknown: 'unknown',
none: 'none',
cellular: 'cellular',
wifi: 'wifi',
bluetooth: 'bluetooth',
ethernet: 'ethernet',
wimax: 'wimax',
vpn: 'vpn',
other: 'other',
};

const RNCNetInfoMock = {
NetInfoStateType,
configure: jest.fn(),
fetch: jest.fn(),
refresh: jest.fn(),
addEventListener: jest.fn(),
useNetInfo: jest.fn(),
getCurrentState: jest.fn(),
};

RNCNetInfoMock.fetch.mockResolvedValue(defaultState);
RNCNetInfoMock.refresh.mockResolvedValue(defaultState);
RNCNetInfoMock.useNetInfo.mockReturnValue(defaultState);
RNCNetInfoMock.addEventListener.mockReturnValue(jest.fn());

export function reactNativeStyleSheetStub() {
const { StyleSheet } = jest.requireActual('react-native');
return jest.spyOn(StyleSheet, 'create');
Expand All @@ -6,9 +44,9 @@ export function reactNativeStyleSheetStub() {
const noopModule = () => ({});

export function removeReactDependencies() {
jest.mock('react', noopModule);
Ariflo marked this conversation as resolved.
Show resolved Hide resolved
jest.mock('react-native-webview', noopModule);
jest.mock('react-native-safe-area-context', noopModule);
jest.mock('@react-native-community/netinfo', () => RNCNetInfoMock);

// The `localforage` driver we use to enable React Native's `AsyncStorage`
// currently uses an `import` statement at the top of it's index file, this is
Expand Down
55 changes: 55 additions & 0 deletions packages/@magic-sdk/react-native-bare/test/spec/hooks.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { act, renderHook } from '@testing-library/react-native';
import NetInfo, { NetInfoStateType } from '@react-native-community/netinfo';
import { useInternetConnection } from '../../src/hooks';

beforeAll(() => {
// @ts-ignore mock resolved value
NetInfo.getCurrentState.mockResolvedValue({
type: NetInfoStateType.cellular,
isConnected: true,
isInternetReachable: true,
details: {
isConnectionExpensive: true,
cellularGeneration: '4g',
},
});
});

describe('useInternetConnection', () => {
it('should initialize with true when connected', async () => {
const { result } = renderHook(() => useInternetConnection());

expect(result.current).toBe(true);
});

it('should call the listener when the connection changes', async () => {
NetInfo.addEventListener = jest.fn();

const { result } = renderHook(() => useInternetConnection());

// Initial render, assuming it's connected
expect(result.current).toBe(true);

// Simulate a change in connection status
act(() => {
Ariflo marked this conversation as resolved.
Show resolved Hide resolved
// @ts-ignore mock calls
NetInfo.addEventListener.mock.calls[0][0]({
type: 'cellular', // Replace with your desired type
isConnected: false,
isInternetReachable: true,
details: {
isConnectionExpensive: true,
// Add other details as needed for your hook
},
});
});

// Wait for the next tick of the event loop to allow state update
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0)); // or setImmediate
});

// Check if the hook state has been updated
expect(result.current).toBe(false);
});
});
30 changes: 30 additions & 0 deletions packages/@magic-sdk/react-native-expo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@ As of `v19.0.0`, passcodes (ie. `loginWithSMS()`, `loginWithEmailOTP()`) are rep
npm install --save @magic-sdk/react-native-expo
npm install --save react-native-webview@^11.26.0 # Required Peer Dependency
npm install --save react-native-safe-area-context # Required Peer Dependency
npm install --save @react-native-community/netinfo # Required Peer Dependency

# Via Yarn:
yarn add @magic-sdk/react-native-expo
yarn add react-native-webview@^11.26.0 # Required Peer Dependency
yarn add react-native-safe-area-context # Required Peer Dependency
yarn add @react-native-community/netinfo # Required Peer Dependency
```

## ⚑️ Quick Start
Expand Down Expand Up @@ -73,3 +75,31 @@ For React Native projects living within a **monorepo** that run into the followi
When attempting to import `Magic`, take note that the React Native metro bundler doesn’t work well with symlinks, which tend to be utilized by most package managers.

For this issue consider using Microsoft's [rnx-kit](https://microsoft.github.io/rnx-kit/docs/guides/bundling) suite of tools that include a plugin for metro that fixes this symlink related error.

### Handling internet connection problems

When the app has internet connectivity issues, the relayer might behave in ways you don't expect. That is why it is important to use [@react-native-community/netinfo](https://www.npmjs.com/package/@react-native-community/netinfo) to track the internet connection state of the device, and refresh your UI when the connection is re-established. For your convenience, we've also added a hook that uses this library behind the scenes:

```tsx
import { useInternetConnection } from '@magic-sdk/react-native-expo';

const magic = new Magic('YOUR_API_KEY');

const connected = useInternetConnection()

useEffect(() => {
if (!connected) {
// Unomount this component and show your "You're offline" screen.
}
}, [connected])

export default function App() {
return <>
<SafeAreaProvider>
{/* Render the Magic iframe! */}
<magic.Relayer />
{...}
</SafeAreaProvider>
</>
}
```
2 changes: 1 addition & 1 deletion packages/@magic-sdk/react-native-expo/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Config } from '@jest/types';

const config: Config.InitialOptions = {
...baseJestConfig,
preset: 'react-native',
preset: '@testing-library/react-native',
transform: {
'^.+\\.(js|jsx)$': 'babel-jest',
'\\.(ts|tsx)$': 'ts-jest',
Expand Down
6 changes: 5 additions & 1 deletion packages/@magic-sdk/react-native-expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,18 @@
"@babel/core": "^7.15.0",
"@babel/plugin-transform-flow-strip-types": "^7.14.5",
"@babel/runtime": "~7.10.4",
"@react-native-community/netinfo": ">11.0.0",
"@testing-library/react-native": "^12.4.0",
"expo-modules-core": "^1.0.4",
"metro-react-native-babel-preset": "^0.66.2",
"react": "^16.13.1",
"react-native": "^0.62.2",
"react-native-safe-area-context": "^4.4.1",
"react-native-webview": ">=12.4.0"
"react-native-webview": ">=12.4.0",
"react-test-renderer": "^16.13.1"
},
"peerDependencies": {
"@react-native-community/netinfo": ">11.0.0",
"expo": "*",
"react": ">=16",
"react-native": ">=0.60",
Expand Down
16 changes: 16 additions & 0 deletions packages/@magic-sdk/react-native-expo/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useEffect, useState } from 'react';
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';

export const useInternetConnection = () => {
const [isConnected, setIsConnected] = useState(true);
useEffect(() => {
const handleConnectionChange = (connectionInfo: NetInfoState) => {
setIsConnected(!!connectionInfo.isConnected);
};

// Subscribe to connection changes
return NetInfo.addEventListener(handleConnectionChange);
}, []);

return isConnected;
};
2 changes: 2 additions & 0 deletions packages/@magic-sdk/react-native-expo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ export type Magic<T extends MagicSDKExtensionsOption<any> = MagicSDKExtensionsOp
SDKBaseReactNative,
T
>;

export { useInternetConnection } from './hooks';
40 changes: 39 additions & 1 deletion packages/@magic-sdk/react-native-expo/test/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,41 @@
// @react-native-community/netinfo mocks
const defaultState = {
type: 'cellular',
isConnected: true,
isInternetReachable: true,
details: {
isConnectionExpensive: true,
cellularGeneration: '3g',
},
};

const NetInfoStateType = {
unknown: 'unknown',
none: 'none',
cellular: 'cellular',
wifi: 'wifi',
bluetooth: 'bluetooth',
ethernet: 'ethernet',
wimax: 'wimax',
vpn: 'vpn',
other: 'other',
};

const RNCNetInfoMock = {
NetInfoStateType,
configure: jest.fn(),
fetch: jest.fn(),
refresh: jest.fn(),
addEventListener: jest.fn(),
useNetInfo: jest.fn(),
getCurrentState: jest.fn(),
};

RNCNetInfoMock.fetch.mockResolvedValue(defaultState);
RNCNetInfoMock.refresh.mockResolvedValue(defaultState);
RNCNetInfoMock.useNetInfo.mockReturnValue(defaultState);
RNCNetInfoMock.addEventListener.mockReturnValue(jest.fn());

export function reactNativeStyleSheetStub() {
const { StyleSheet } = jest.requireActual('react-native');
return jest.spyOn(StyleSheet, 'create');
Expand All @@ -6,9 +44,9 @@ export function reactNativeStyleSheetStub() {
const noopModule = () => ({});

export function removeReactDependencies() {
jest.mock('react', noopModule);
jest.mock('react-native-webview', noopModule);
jest.mock('react-native-safe-area-context', noopModule);
jest.mock('@react-native-community/netinfo', () => RNCNetInfoMock);

// The `localforage` driver we use to enable React Native's `AsyncStorage`
// currently uses an `import` statement at the top of it's index file, this is
Expand Down
Loading