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

Use storage for faster calls to isLoggedIn on mobile #689

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
abb9059
Use storage for faster calls to isLoggedIn
romin-halltari Dec 20, 2023
603c9b6
Merge remote-tracking branch 'origin/master' into rominhalltari-sc-91…
romin-halltari Dec 20, 2023
066e22a
Fix: add options argument to createMagicSDK
romin-halltari Dec 20, 2023
a2c8148
Remove unnecessary promiEvent variable
romin-halltari Dec 20, 2023
c85f142
[expo] add unit tests for storage cache changes
romin-halltari Dec 20, 2023
1c48262
[react-native-bare] Use storage for faster calls to isLoggedIn
romin-halltari Dec 20, 2023
a752f1e
Revert "[react-native-bare] Use storage for faster calls to isLoggedIn"
romin-halltari Dec 21, 2023
9641d2e
Revert changes to react-native-bare
romin-halltari Dec 21, 2023
97f3230
Use storage for faster calls to isLoggedIn for both mobile and web
romin-halltari Dec 21, 2023
b2f80c0
Check if useStorageCache is true
romin-halltari Dec 21, 2023
dda3e50
Merge remote-tracking branch 'origin/master' into rominhalltari-sc-91…
romin-halltari Dec 21, 2023
8410ec7
Revert changes to react-native-bare
romin-halltari Dec 21, 2023
b24c775
Revert changes to react-native-expo
romin-halltari Dec 21, 2023
8a1bbe7
Rename is_logged_in -> magic_auth_is_logged_in
romin-halltari Dec 21, 2023
619c70a
Flatten if condition
romin-halltari Dec 21, 2023
0910dfc
Merge remote-tracking branch 'origin/master' into rominhalltari-sc-91…
romin-halltari Dec 22, 2023
1c81174
update yarn.lock
romin-halltari Dec 22, 2023
3bfc8dc
Add unit test about deprecation warning to increase test coverage
romin-halltari Dec 22, 2023
4b83a77
Merge remote-tracking branch 'origin/master' into rominhalltari-sc-91…
Jan 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/@magic-sdk/provider/src/core/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ export interface MagicSDKAdditionalConfiguration<
extensions?: TExt;
testMode?: boolean;
deferPreload?: boolean;
useStorageCache?: boolean;
}

export class SDKBase {
Expand All @@ -126,6 +127,7 @@ export class SDKBase {
protected readonly parameters: string;
protected readonly networkHash: string;
public readonly testMode: boolean;
public readonly useStorageCache: boolean;

/**
* Contains methods for starting a Magic SDK authentication flow.
Expand Down Expand Up @@ -167,6 +169,7 @@ export class SDKBase {

const { defaultEndpoint, version } = SDKEnvironment;
this.testMode = !!options?.testMode;
this.useStorageCache = !!options?.useStorageCache;
this.endpoint = createURL(options?.endpoint ?? defaultEndpoint).origin;

// Prepare built-in modules
Expand Down
74 changes: 65 additions & 9 deletions packages/@magic-sdk/provider/src/modules/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import {
RecoverAccountConfiguration,
ShowSettingsConfiguration,
} from '@magic-sdk/types';
import { getItem, removeItem } from '../util/storage';
import { getItem, setItem, removeItem } from '../util/storage';
import { BaseModule } from './base-module';
import { createJsonRpcRequestPayload } from '../core/json-rpc';
import { createDeprecationWarning } from '../core/sdk-exceptions';
import { ProductConsolidationMethodRemovalVersions } from './auth';
import { clearDeviceShares } from '../util/device-share-web-crypto';
import { createPromiEvent } from '../util';

export type UpdateEmailEvents = {
'email-sent': () => void;
Expand All @@ -23,6 +24,9 @@ export type UpdateEmailEvents = {
'new-email-confirmed': () => void;
retry: () => void;
};

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

export class UserModule extends BaseModule {
public getIdToken(configuration?: GetIdTokenConfiguration) {
const requestPayload = createJsonRpcRequestPayload(
Expand All @@ -47,19 +51,59 @@ export class UserModule extends BaseModule {
}

public isLoggedIn() {
const requestPayload = createJsonRpcRequestPayload(
this.sdk.testMode ? MagicPayloadMethod.IsLoggedInTestMode : MagicPayloadMethod.IsLoggedIn,
);
return this.request<boolean>(requestPayload);
return createPromiEvent<boolean, any>(async (resolve, reject) => {
try {
let cachedIsLoggedIn = false;
if (this.sdk.useStorageCache) {
cachedIsLoggedIn = (await getItem(this.localForageIsLoggedInKey)) === '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);
romin-halltari marked this conversation as resolved.
Show resolved Hide resolved
}
}

const requestPayload = createJsonRpcRequestPayload(
this.sdk.testMode ? MagicPayloadMethod.IsLoggedInTestMode : MagicPayloadMethod.IsLoggedIn,
);
const isLoggedInResponse = await this.request<boolean>(requestPayload);
if (this.sdk.useStorageCache) {
if (isLoggedInResponse) {
setItem(this.localForageIsLoggedInKey, true);
} else {
removeItem(this.localForageIsLoggedInKey);
}
if (cachedIsLoggedIn && !isLoggedInResponse) {
this.emitUserLoggedOut(true);
}
}
resolve(isLoggedInResponse);
} catch (err) {
reject(err);
}
});
}

public logout() {
removeItem(this.localForageKey);
removeItem(this.localForageIsLoggedInKey);
clearDeviceShares();
const requestPayload = createJsonRpcRequestPayload(
this.sdk.testMode ? MagicPayloadMethod.LogoutTestMode : MagicPayloadMethod.Logout,
);
return this.request<boolean>(requestPayload);

return createPromiEvent<boolean, any>(async (resolve, reject) => {
try {
const requestPayload = createJsonRpcRequestPayload(
this.sdk.testMode ? MagicPayloadMethod.LogoutTestMode : MagicPayloadMethod.Logout,
);
const response = await this.request<boolean>(requestPayload);
if (this.sdk.useStorageCache) {
this.emitUserLoggedOut(response);
}
resolve(response);
} catch (err) {
reject(err);
}
});
}

/* Request email address from logged in user */
Expand Down Expand Up @@ -112,6 +156,18 @@ export class UserModule extends BaseModule {
return this.request<string | null, UpdateEmailEvents>(requestPayload);
}

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

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

private localForageKey = 'mc_active_wallet';
private localForageIsLoggedInKey = 'magic_auth_is_logged_in';
private userLoggedOutCallbacks: UserLoggedOutCallback[] = [];
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/* eslint-disable global-require, @typescript-eslint/no-var-requires */

import browserEnv from '@ikscodes/browser-env';
import { MagicPayloadMethod } from '@magic-sdk/types';
import { MagicPayloadMethod, SDKWarningCode } from '@magic-sdk/types';

import { SDKEnvironment } from '../../../../src/core/sdk-environment';
import { isPromiEvent } from '../../../../src/util';
import { createMagicSDK, createMagicSDKTestMode } from '../../../factories';
import * as SdkExceptions from '../../../../src/core/sdk-exceptions';
import { ProductConsolidationMethodRemovalVersions } from '../../../../src/modules/auth';

beforeEach(() => {
browserEnv.restore();
Expand Down Expand Up @@ -97,3 +99,25 @@ test('Throws error when the SDK version is 19 or higher', async () => {
);
}
});

test('Creates deprecation warning if ran on a react native environment with version < 19', async () => {
const magic = createMagicSDK();
magic.auth.request = jest.fn();
const deprecationWarnStub = jest.spyOn(SdkExceptions, 'createDeprecationWarning').mockReturnValue({
log: jest.fn(),
message: 'test',
code: SDKWarningCode.DeprecationNotice,
rawMessage: 'test',
});

// Set SDKEnvironment version to 18
SDKEnvironment.version = '18';
SDKEnvironment.sdkName = '@magic-sdk/react-native';

await magic.auth.loginWithMagicLink({ email: 'test' });
expect(deprecationWarnStub).toHaveBeenCalledWith({
method: 'auth.loginWithMagicLink()',
removalVersions: ProductConsolidationMethodRemovalVersions,
useInstead: 'auth.loginWithEmailOTP()',
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import browserEnv from '@ikscodes/browser-env';
import { createMagicSDK } from '../../../factories';

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

test('emitUserLoggedOut emits event', () => {
const magic = createMagicSDK();
const callbackMock = jest.fn();
magic.user.onUserLoggedOut(callbackMock);
magic.user.emitUserLoggedOut(true);
expect(callbackMock).toHaveBeenCalledWith(true);
});
Original file line number Diff line number Diff line change
@@ -1,17 +1,60 @@
import browserEnv from '@ikscodes/browser-env';
import { createMagicSDK, createMagicSDKTestMode } from '../../../factories';
import { BaseModule } from '../../../../src/modules/base-module';
import { isPromiEvent } from '../../../../src/util';
import { isPromiEvent, storage } from '../../../../src/util';
import { mockLocalForage } from '../../../mocks';

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

test('Resolves immediately when cached magic_auth_is_logged_in is true', async () => {
mockLocalForage({ magic_auth_is_logged_in: 'true' });
const magic = createMagicSDK();
magic.useStorageCache = true;

const isLoggedIn = await magic.user.isLoggedIn();

expect(isLoggedIn).toEqual(true);
});

test('Waits for request before resolving when cached magic_auth_is_logged_in is false', async () => {
mockLocalForage({ magic_auth_is_logged_in: 'true' });
const magic = createMagicSDK();
magic.user.request = jest.fn().mockResolvedValue(true);

const isLoggedIn = await magic.user.isLoggedIn();

expect(isLoggedIn).toEqual(true);
});

test('Stores magic_auth_is_logged_in=true in local storage when request resolves true', async () => {
mockLocalForage();
const magic = createMagicSDK();
magic.useStorageCache = true;
magic.user.request = jest.fn().mockResolvedValue(true);

await magic.user.isLoggedIn();

expect(storage.setItem).toHaveBeenCalledWith('magic_auth_is_logged_in', true);
});

test('Removes magic_auth_is_logged_in=true from local storage when request resolves false', async () => {
mockLocalForage();
const magic = createMagicSDK();
magic.useStorageCache = true;
magic.user.request = jest.fn().mockResolvedValue(false);

await magic.user.isLoggedIn();

expect(storage.removeItem).toHaveBeenCalledWith('magic_auth_is_logged_in');
});

test(' Generate JSON RPC request payload with method `magic_is_logged_in`', async () => {
const magic = createMagicSDK();
magic.user.request = jest.fn();

magic.user.isLoggedIn();
await magic.user.isLoggedIn();

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_is_logged_in');
Expand All @@ -22,7 +65,7 @@ test('If `testMode` is enabled, testing-specific RPC method is used', async () =
const magic = createMagicSDKTestMode();
magic.user.request = jest.fn();

magic.user.isLoggedIn();
await magic.user.isLoggedIn();

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_auth_is_logged_in_testing_mode');
Expand All @@ -33,3 +76,23 @@ test('method should return a PromiEvent', () => {
const magic = createMagicSDK();
expect(isPromiEvent(magic.user.isLoggedIn())).toBeTruthy();
});

test('Should reject with error if error occurs', async () => {
const magic = createMagicSDKTestMode();
magic.user.request = jest.fn().mockRejectedValue(new Error('something went wrong'));

await expect(magic.user.isLoggedIn()).rejects.toThrowError(new Error('something went wrong'));
});

test('Emits user logged out event when logout resolves', async () => {
mockLocalForage({ magic_auth_is_logged_in: 'true' });
const magic = createMagicSDK();
magic.useStorageCache = true;
magic.user.request = jest.fn().mockResolvedValue(true);

const spyEmitUserLoggedOut = jest.spyOn(magic.user, 'emitUserLoggedOut');

await magic.user.logout();

expect(spyEmitUserLoggedOut).toHaveBeenCalledWith(true);
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import browserEnv from '@ikscodes/browser-env';
import { isPromiEvent } from '../../../../src/util';
import { isPromiEvent, storage } from '../../../../src/util';
import { createMagicSDK, createMagicSDKTestMode } from '../../../factories';
import { mockLocalForage } from '../../../mocks';

beforeEach(() => {
browserEnv.restore();
Expand Down Expand Up @@ -32,3 +33,28 @@ test('If `testMode` is enabled, testing-specific RPC method is used', async () =
expect(requestPayload.method).toBe('magic_auth_logout_testing_mode');
expect(requestPayload.params).toEqual([]);
});

test('Removes magic_auth_is_logged_in from local storage', async () => {
const magic = createMagicSDKTestMode();
mockLocalForage({ magic_auth_is_logged_in: 'true' });

magic.user.logout();

expect(storage.removeItem).toHaveBeenCalledWith('magic_auth_is_logged_in');
});

test('Removes mc_active_wallet from local storage', async () => {
const magic = createMagicSDKTestMode();
mockLocalForage({ mc_active_wallet: 'test' });

magic.user.logout();

expect(storage.removeItem).toHaveBeenCalledWith('mc_active_wallet');
});

test('Should reject with error if error occurs', async () => {
const magic = createMagicSDKTestMode();
magic.user.request = jest.fn().mockRejectedValue(new Error('something went wrong'));

await expect(magic.user.logout()).rejects.toThrowError(new Error('something went wrong'));
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import browserEnv from '@ikscodes/browser-env';
import { createMagicSDK } from '../../../factories';

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

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