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 UserModule#settings endpoint #231

Merged
merged 7 commits into from
Oct 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
72 changes: 52 additions & 20 deletions packages/@magic-sdk/pnp/src/context/callback.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import { createMagicInstance, getScriptData } from '../utils';
import { getScriptData } from '../utils/script-data';
import { createMagicInstance } from '../utils/magic-instance';
import { dispatchReadyEvent } from '../utils/events';

export async function callback(): Promise<void> {
const { src, apiKey } = getScriptData();
// In this context, `loginURI` and `redirectURI` are the same.
// We simply need a location to redirect to upon callback failure.
const { src, apiKey, loginURI, redirectURI = window.location.origin } = getScriptData();

const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);

const isOAuthCallback = !!urlParams.get('state');
const isMagicLinkRedirectCallback = !isOAuthCallback && !!urlParams.get('magic_credential');

const magic = createMagicInstance(apiKey, src.origin);

function clearURLQuery() {
const urlWithoutQuery = window.location.origin + window.location.pathname;
window.history.replaceState(null, '', urlWithoutQuery);
}

function dispatchReadyEvent(data: any) {
const evt = new CustomEvent('@magic/ready', { detail: { magic, ...data } });
window.dispatchEvent(evt);
}

async function handleOAuthCallback() {
const res = await magic.oauth.getRedirectResult();
dispatchReadyEvent({
dispatchReadyEvent(magic, {
idToken: res.magic.idToken,
userMetadata: res.magic.userMetadata,
oauth: res.oauth,
Expand All @@ -33,7 +28,16 @@ export async function callback(): Promise<void> {
async function handleMagicLinkRedirectCallback() {
const idToken = await magic.auth.loginWithCredential();
const userMetadata = await magic.user.getMetadata();
dispatchReadyEvent({ idToken, userMetadata });
dispatchReadyEvent(magic, { idToken, userMetadata });
}

async function handleSettingsCallback() {
const idToken = await magic.user.getIdToken();
const prevUserMetadata = magic.pnp.decodeUserMetadata(urlParams.get('prev_user_metadata')) ?? undefined;
const currUserMetadata =
magic.pnp.decodeUserMetadata(urlParams.get('curr_user_metadata')) ?? (await magic.user.getMetadata());
clearURLQuery();
dispatchReadyEvent(magic, { idToken, userMetadata: currUserMetadata, prevUserMetadata });
}

/**
Expand All @@ -50,14 +54,42 @@ export async function callback(): Promise<void> {
const idToken = urlParams.get('didt') || (await magic.user.getIdToken());
clearURLQuery();
const userMetadata = await magic.user.getMetadata();
dispatchReadyEvent({ idToken: decodeURIComponent(idToken), userMetadata });
dispatchReadyEvent(magic, { idToken: decodeURIComponent(idToken), userMetadata });
}

const redirectToLoginURI = () => {
window.location.href = loginURI || redirectURI;
};

switch (getCallbackType(urlParams)) {
case 'oauth':
return handleOAuthCallback().catch(redirectToLoginURI);

case 'magic_credential':
return handleMagicLinkRedirectCallback().catch(redirectToLoginURI);

case 'settings':
return handleSettingsCallback().catch(redirectToLoginURI);

default:
return handleGenericCallback().catch(redirectToLoginURI);
}
}

type CallbackType = 'oauth' | 'magic_credential' | 'settings';

function getCallbackType(urlParams: URLSearchParams): CallbackType | null {
if (urlParams.get('state')) {
return 'oauth';
}

if (isOAuthCallback) {
await handleOAuthCallback().catch(() => {});
} else if (isMagicLinkRedirectCallback) {
await handleMagicLinkRedirectCallback().catch(() => {});
} else {
await handleGenericCallback().catch(() => {});
if (urlParams.get('magic_credential')) {
return 'magic_credential';
}

if (urlParams.get('prev_user_metadata')) {
return 'settings';
}

return null;
}
3 changes: 2 additions & 1 deletion packages/@magic-sdk/pnp/src/context/login.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createMagicInstance, getScriptData } from '../utils';
import { getScriptData } from '../utils/script-data';
import { createMagicInstance } from '../utils/magic-instance';

export async function login(): Promise<void> {
const { src, apiKey, redirectURI = `${window.location.origin}/callback`, debug } = getScriptData();
Expand Down
9 changes: 6 additions & 3 deletions packages/@magic-sdk/pnp/src/context/logout.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { createMagicInstance, getScriptData } from '../utils';
import { getScriptData } from '../utils/script-data';
import { createMagicInstance } from '../utils/magic-instance';

export async function logout(): Promise<void> {
const { src, apiKey, redirectURI = window.location.origin } = getScriptData();
// In this context, `loginURI` and `redirectURI` are the same.
// We simply need a location to redirect to after attempting to logout.
const { src, apiKey, loginURI, redirectURI = window.location.origin } = getScriptData();
const magic = createMagicInstance(apiKey, src.origin);
await magic.user.logout().catch(() => {});
window.location.href = redirectURI;
window.location.href = loginURI || redirectURI;
}
24 changes: 24 additions & 0 deletions packages/@magic-sdk/pnp/src/context/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { getScriptData } from '../utils/script-data';
import { createMagicInstance } from '../utils/magic-instance';

export async function settings(): Promise<void> {
// In this context, `loginURI` and `redirectURI` have distinct purposes.
// `loginURI` is the location we redirect to when calling into settings fails.
// `redirectURI` is the location we redirect to after the user has dismissed the settings page.
const {
src,
apiKey,
redirectURI = `${window.location.origin}/callback`,
loginURI = window.location.origin,
} = getScriptData();

const magic = createMagicInstance(apiKey, src.origin);

try {
const prevUserMetadata = magic.pnp.encodeUserMetadata(await magic.user.getMetadata());
const currUserMetadata = magic.pnp.encodeUserMetadata(await magic.pnp.showSettings());
window.location.href = `${redirectURI}?prev_user_metadata=${prevUserMetadata}&curr_user_metadata=${currUserMetadata}`;
} catch {
window.location.href = loginURI;
}
}
8 changes: 7 additions & 1 deletion packages/@magic-sdk/pnp/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/* eslint-disable no-useless-return */
/* eslint-disable consistent-return */

import { getScriptData } from './utils';
import { getScriptData } from './utils/script-data';

// PnP contexts
import { login } from './context/login';
import { logout } from './context/logout';
import { callback } from './context/callback';
import { settings } from './context/settings';

function main() {
const { src } = getScriptData();
Expand All @@ -19,6 +22,9 @@ function main() {
case '/pnp/callback':
return callback();

case '/pnp/settings':
return settings();

default:
return;
}
Expand Down
23 changes: 19 additions & 4 deletions packages/@magic-sdk/pnp/src/pnp-extension.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { MagicUserMetadata } from '@magic-sdk/types';

export class PlugNPlayExtension extends window.Magic.Extension.Internal<'pnp', { isPnP: boolean }> {
config = { isPnP: true };
name = 'pnp' as const;

static storageKeys = {
public static storageKeys = {
lastUsedProvider: 'pnp/lastUsedProvider',
};

getLoginMethod(debug?: boolean) {
return this.utils.createPromiEvent<[string, string | undefined], { yolo: () => void }>(async (resolve) => {
public getLoginMethod(debug?: boolean) {
return this.utils.createPromiEvent<[string, string | undefined]>(async (resolve) => {
const lastUsedProvider = await this.utils.storage.getItem<string | undefined>(
PlugNPlayExtension.storageKeys.lastUsedProvider,
);
Expand All @@ -16,9 +18,22 @@ export class PlugNPlayExtension extends window.Magic.Extension.Internal<'pnp', {
});
}

async saveLastUsedProvider(provider?: string) {
public showSettings() {
return this.request(this.utils.createJsonRpcRequestPayload('pnp/settings'));
}

public async saveLastUsedProvider(provider?: string) {
if (provider) {
await this.utils.storage.setItem(PlugNPlayExtension.storageKeys.lastUsedProvider, provider);
}
}

public encodeUserMetadata(userMetadata: MagicUserMetadata) {
return this.utils.encodeJSON(userMetadata);
}

public decodeUserMetadata(userMetadataQueryString?: string | null): MagicUserMetadata | null {
if (!userMetadataQueryString) return null;
return this.utils.decodeJSON(userMetadataQueryString);
}
}
38 changes: 0 additions & 38 deletions packages/@magic-sdk/pnp/src/utils.ts

This file was deleted.

6 changes: 6 additions & 0 deletions packages/@magic-sdk/pnp/src/utils/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { PNPMagicInstance } from './magic-instance';

export function dispatchReadyEvent(magic: PNPMagicInstance, data: any = {}) {
const evt = new CustomEvent('@magic/ready', { detail: { magic, ...data } });
window.dispatchEvent(evt);
}
20 changes: 20 additions & 0 deletions packages/@magic-sdk/pnp/src/utils/magic-instance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { PlugNPlayExtension } from '../pnp-extension';
import type { Magic, OAuthExtension } from '../types';

export function createMagicInstance(apiKey?: string, endpoint?: string): Magic<[PlugNPlayExtension, OAuthExtension]> {
const extensions = removeFalsey([
new PlugNPlayExtension(),
window.MagicOAuthExtension && new window.MagicOAuthExtension(),
]);

return new window.Magic(apiKey!, {
endpoint,
extensions,
});
}

export type PNPMagicInstance = ReturnType<typeof createMagicInstance>;

function removeFalsey<T>(arr: T[]): Array<NonNullable<T>> {
return arr.filter(Boolean) as unknown as Array<NonNullable<T>>;
}
24 changes: 24 additions & 0 deletions packages/@magic-sdk/pnp/src/utils/script-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const allPossiblePNPScripts = document.querySelectorAll('script[data-magic-publishable-api-key]');
const thisScript = (document.currentScript ??
allPossiblePNPScripts[allPossiblePNPScripts.length - 1]) as HTMLScriptElement;

export function getScriptData() {
const src = new URL(thisScript.getAttribute('src')!);
const apiKey = thisScript.dataset.magicPublishableApiKey;
const debug = !!thisScript.dataset.debug;
const redirectURI = getAbsoluteURL(thisScript.dataset.redirectUri);
const loginURI = getAbsoluteURL(thisScript.dataset.loginUri);

return { script: thisScript, src, apiKey, redirectURI, loginURI, debug };
}

export type ScriptData = ReturnType<typeof getScriptData>;

/**
* Resolve `relativeURL` to an absolute path against current origin by
* (naively) checking if `relativeURL` starts with "/".
*/
function getAbsoluteURL(relativeURL?: string) {
if (relativeURL?.startsWith('/')) return `${window.location.origin}${relativeURL}`;
return relativeURL;
}
8 changes: 8 additions & 0 deletions packages/@magic-sdk/provider/src/modules/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,12 @@ export class UserModule extends BaseModule {
);
return this.request<boolean>(requestPayload);
}

/** */
public showSettings() {
const requestPayload = createJsonRpcRequestPayload(
this.sdk.testMode ? MagicPayloadMethod.UserSettingsTestMode : MagicPayloadMethod.UserSettings,
);
return this.request<MagicUserMetadata>(requestPayload);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import browserEnv from '@ikscodes/browser-env';
import { createMagicSDK, createMagicSDKTestMode } from '../../../factories';
import { isPromiEvent } from '../../../../src/util';

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

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

magic.user.showSettings();

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_auth_settings');
expect(requestPayload.params).toEqual([]);
});

test('If `testMode` is enabled, testing-specific RPC method is used', async () => {
const magic = createMagicSDKTestMode();
magic.user.request = jest.fn();

magic.user.showSettings();

const requestPayload = magic.user.request.mock.calls[0][0];
expect(requestPayload.method).toBe('magic_auth_settings_testing_mode');
expect(requestPayload.params).toEqual([]);
});

test('method should return a PromiEvent', () => {
const magic = createMagicSDK();
expect(isPromiEvent(magic.user.showSettings())).toBeTruthy();
});
2 changes: 2 additions & 0 deletions packages/@magic-sdk/types/src/core/json-rpc-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export enum MagicPayloadMethod {
IsLoggedIn = 'magic_auth_is_logged_in',
Logout = 'magic_auth_logout',
UpdateEmail = 'magic_auth_update_email',
UserSettings = 'magic_auth_settings',
UserSettingsTestMode = 'magic_auth_settings_testing_mode',
LoginWithSmsTestMode = 'magic_auth_login_with_sms_testing_mode',
LoginWithMagicLinkTestMode = 'magic_login_with_magic_link_testing_mode',
LoginWithCredentialTestMode = 'magic_auth_login_with_credential_testing_mode',
Expand Down