Skip to content

Commit

Permalink
[react-native-bare] Use storage for faster calls to isLoggedIn
Browse files Browse the repository at this point in the history
  • Loading branch information
romin-halltari committed Dec 20, 2023
1 parent c85f142 commit 1c48262
Show file tree
Hide file tree
Showing 5 changed files with 178 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -1,7 +1,70 @@
import { SDKBase } from '@magic-sdk/provider';
import { createPromiEvent, MagicSDKAdditionalConfiguration, SDKBase, UserModule } from '@magic-sdk/provider';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { ReactNativeWebViewController } from './react-native-webview-controller';

type UserLoggedOutCallback = (loggedOut: boolean) => void;

export class SDKBaseReactNative extends SDKBase {
// this UserModule instance used for the actual calls to
// `isLoggedIn` and `logOut`, given that we assign new functions
// to `this.user.isLoggedIn` and `this.user.logOut`.
private usr: UserModule;
private userLoggedOutCallbacks: UserLoggedOutCallback[] = [];

constructor(public readonly apiKey: string, options?: MagicSDKAdditionalConfiguration) {
super(apiKey, options);
this.usr = new UserModule(this);
if (options?.useStorageCacheMobile) {
this.user.isLoggedIn = () => {
return createPromiEvent<boolean, any>(async (resolve, reject) => {
const cachedIsLoggedIn = (await AsyncStorage.getItem('isLoggedIn')) === 'true';

// if isLoggedIn is true on storage, optimistically resolve with true
// if it is false, we use `usr.isLoggedIn` as the source of truth.
if (cachedIsLoggedIn) {
resolve(true);
}

this.usr.isLoggedIn().then(async (isLoggedIn: boolean) => {
if (isLoggedIn === false) {
AsyncStorage.removeItem('isLoggedIn');
if (cachedIsLoggedIn) {
this.emitUserLoggedOut(true);
}
} else {
AsyncStorage.setItem('isLoggedIn', 'true');
}
resolve(isLoggedIn);
});
});
};

this.user.logout = () => {
return createPromiEvent<boolean, any>(async (resolve, reject) => {
let response = false;
try {
response = await this.usr.logout();
await AsyncStorage.removeItem('isLoggedIn');
this.emitUserLoggedOut(response);
resolve(response);
} catch (e) {
reject(e);
}
});
};
}
}

private emitUserLoggedOut(loggedOut: boolean): void {
this.userLoggedOutCallbacks.forEach((callback) => {
callback(loggedOut);
});
}

public onUserLoggedOut(callback: UserLoggedOutCallback): void {
this.userLoggedOutCallbacks.push(callback);
}

public get Relayer() {
return (this.overlay as ReactNativeWebViewController).Relayer;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/@magic-sdk/react-native-bare/test/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import AsyncStorageMock from '@react-native-async-storage/async-storage/jest/async-storage-mock';

// @react-native-community/netinfo mocks
const defaultState = {
type: 'cellular',
Expand Down Expand Up @@ -62,3 +64,5 @@ export function removeReactDependencies() {
};
});
}

jest.mock('@react-native-async-storage/async-storage', () => AsyncStorageMock);
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import browserEnv from '@ikscodes/browser-env';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { UserModule } from '@magic-sdk/provider';
import { createMagicSDK } from '../../factories';
import { reactNativeStyleSheetStub } from '../../mocks';

beforeEach(() => {
browserEnv.restore();
reactNativeStyleSheetStub();
});

test('SDKBaseReactNative constructor sets up UserModule instance when useStorageCacheMobile is true', () => {
const magic = createMagicSDK({ useStorageCacheMobile: true });

// Ensure that the UserModule instance is created
expect(magic.usr).toBeDefined();
expect(magic.usr).toBeInstanceOf(UserModule);
});

test('Returns true if the cached value of isLoggedIn is true', async () => {
const magic = createMagicSDK({ useStorageCacheMobile: true });

AsyncStorage.getItem = jest.fn().mockResolvedValue('true');
const isLoggedInResult = await magic.user.isLoggedIn();

expect(AsyncStorage.getItem).toHaveBeenCalledWith('isLoggedIn');
expect(isLoggedInResult).toBe(true);
});

test('Saves isLoggedIn=true in cache when user is logged in', async () => {
const magic = createMagicSDK({ useStorageCacheMobile: true });

AsyncStorage.getItem = jest.fn().mockResolvedValue('false');
jest.spyOn(magic.usr, 'isLoggedIn').mockResolvedValue(true);
const isLoggedInResult = await magic.user.isLoggedIn();

expect(AsyncStorage.getItem).toHaveBeenCalledWith('isLoggedIn');
expect(AsyncStorage.setItem).toHaveBeenCalledWith('isLoggedIn', 'true');
expect(isLoggedInResult).toBe(true);
});

test('Removes isLoggedIn from cache when user is not logged in', async () => {
const magic = createMagicSDK({ useStorageCacheMobile: true });

AsyncStorage.getItem = jest.fn().mockResolvedValue('true');
jest.spyOn(magic.usr, 'isLoggedIn').mockResolvedValue(false);
const isLoggedInResult = await magic.user.isLoggedIn();

expect(AsyncStorage.getItem).toHaveBeenCalledWith('isLoggedIn');
expect(AsyncStorage.removeItem).toHaveBeenCalledWith('isLoggedIn');
expect(isLoggedInResult).toBe(true);
});

test('Removes isLoggedIn from cache user logs out', async () => {
const magic = createMagicSDK({ useStorageCacheMobile: true });

const emitUserLoggedOutSpy = jest.spyOn(magic, 'emitUserLoggedOut');
AsyncStorage.getItem = jest.fn().mockResolvedValue('false');
jest.spyOn(magic.usr, 'logout').mockResolvedValue(true);
const isLoggedOut = await magic.user.logout();

expect(AsyncStorage.removeItem).toHaveBeenCalledWith('isLoggedIn');
expect(emitUserLoggedOutSpy).toHaveBeenCalledWith(isLoggedOut);
expect(isLoggedOut).toBe(true);
});

test('Rejects with error if an error is thrown', async () => {
const magic = createMagicSDK({ useStorageCacheMobile: true });

const emitUserLoggedOutSpy = jest.spyOn(magic, 'emitUserLoggedOut');
AsyncStorage.getItem = jest.fn().mockResolvedValue('false');
jest.spyOn(magic.usr, 'logout').mockRejectedValue(new Error('something went wrong'));

await expect(magic.user.logout()).rejects.toThrowError('something went wrong');
expect(AsyncStorage.removeItem).toHaveBeenCalledWith('isLoggedIn');
expect(emitUserLoggedOutSpy).not.toHaveBeenCalled(); // Since logout threw an error, emitUserLoggedOut should not be called
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import browserEnv from '@ikscodes/browser-env';
import { createMagicSDK } from '../../factories';
import { reactNativeStyleSheetStub } from '../../mocks';

beforeEach(() => {
browserEnv.restore();
reactNativeStyleSheetStub();
});

test('emitUserLoggedOut emits event', () => {
const magic = createMagicSDK();
const callbackMock = jest.fn();
magic.onUserLoggedOut(callbackMock);
magic.emitUserLoggedOut(true);
expect(callbackMock).toHaveBeenCalledWith(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import browserEnv from '@ikscodes/browser-env';
import { createMagicSDK } from '../../factories';
import { reactNativeStyleSheetStub } from '../../mocks';

beforeEach(() => {
browserEnv.restore();
reactNativeStyleSheetStub();
});

test('onUserLoggedOut adds callback', () => {
const magic = createMagicSDK();
const callbackMock = jest.fn();
magic.onUserLoggedOut(callbackMock);
const callbacks = magic.userLoggedOutCallbacks;
expect(callbacks).toHaveLength(1);
expect(callbacks[0]).toBe(callbackMock);
});

0 comments on commit 1c48262

Please sign in to comment.