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

MagicV2 + Farcaster login + SMS login #9913

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions libs/shared/src/types/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ export enum WalletSsoSource {
Twitter = 'twitter',
Apple = 'apple',
Email = 'email',
Farcaster = 'farcaster',
SMS = 'SMS',
Unknown = 'unknown', // address created after we launched SSO, before we started recording WalletSsoSource
}

Expand Down
138 changes: 96 additions & 42 deletions packages/commonwealth/client/scripts/controllers/app/login.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
/**
* @file Manages logged-in user accounts and local storage.
*/
import { SIWESigner } from '@canvas-js/chain-ethereum';
import { Session } from '@canvas-js/interfaces';

import { ExtendedCommunity } from '@hicommonwealth/schemas';
import {
CANVAS_TOPIC,
ChainBase,
chainBaseToCanvasChainId,
getSessionSigners,
serializeCanvas,
WalletId,
WalletSsoSource,
chainBaseToCanvasChainId,
} from '@hicommonwealth/shared';
import { CosmosExtension } from '@magic-ext/cosmos';
import { FarcasterExtension } from '@magic-ext/farcaster';
import { OAuthExtension } from '@magic-ext/oauth';
import { OAuthExtension as OAuthExtensionV2 } from '@magic-ext/oauth2';
import axios from 'axios';
import { notifyError } from 'controllers/app/notifications';
import { getMagicCosmosSessionSigner } from 'controllers/server/sessions';
import { isSameAccount } from 'helpers';

import { getSessionSigners } from '@hicommonwealth/shared';
import { initAppState } from 'state';

import { SIWESigner } from '@canvas-js/chain-ethereum';
import { Session } from '@canvas-js/interfaces';
import { CANVAS_TOPIC, serializeCanvas } from '@hicommonwealth/shared';
import { CosmosExtension } from '@magic-ext/cosmos';
import { OAuthExtension } from '@magic-ext/oauth';
import { Magic } from 'magic-sdk';

import { ExtendedCommunity } from '@hicommonwealth/schemas';
import axios from 'axios';
import app from 'state';
import app, { initAppState } from 'state';
import { EXCEPTION_CASE_VANILLA_getCommunityById } from 'state/api/communities/getCommuityById';
import { SERVER_URL } from 'state/api/config';
import {
Expand All @@ -37,6 +37,17 @@ import { z } from 'zod';
import Account from '../../models/Account';
import AddressInfo from '../../models/AddressInfo';
import type BlockInfo from '../../models/BlockInfo';
import { fetchCachedCustomDomain } from '../../state/api/configuration/index';

// need to instantiate it early because the farcaster sdk has an async constructor which will cause a race condition
// if instantiated right before the login is called;
export const defaultMagic = new Magic(process.env.MAGIC_PUBLISHABLE_KEY!, {
extensions: [
new FarcasterExtension(),
new OAuthExtension(),
new OAuthExtensionV2(),
],
});

function storeActiveAccount(account: Account) {
const user = userStore.getState();
Expand Down Expand Up @@ -287,42 +298,48 @@ export async function createUserWithAddress(
}

async function constructMagic(isCosmos: boolean, chain?: string) {
if (!isCosmos) {
return defaultMagic;
}

if (isCosmos && !chain) {
throw new Error('Must be in a community to sign in with Cosmos magic link');
}

if (process.env.MAGIC_PUBLISHABLE_KEY === undefined) {
throw new Error('Missing magic key');
}

return new Magic(process.env.MAGIC_PUBLISHABLE_KEY, {
extensions: !isCosmos
? [new OAuthExtension()]
: [
new OAuthExtension(),
new CosmosExtension({
// Magic has a strict cross-origin policy that restricts rpcs to whitelisted URLs,
// so we can't use app.chain.meta?.node?.url
rpcUrl: `${document.location.origin}${SERVER_URL}/magicCosmosProxy/${chain}`,
}),
],
extensions: [
new OAuthExtension(),
new OAuthExtensionV2(),
new CosmosExtension({
// Magic has a strict cross-origin policy that restricts rpcs to whitelisted URLs,
// so we can't use app.chain.meta?.node?.url
rpcUrl: `${document.location.origin}${SERVER_URL}/magicCosmosProxy/${chain}`,
}),
],
});
}

export async function startLoginWithMagicLink({
email,
phoneNumber,
provider,
redirectTo,
chain,
isCosmos,
}: {
email?: string;
phoneNumber?: string;
provider?: WalletSsoSource;
redirectTo?: string;
chain?: string;
isCosmos: boolean;
}) {
if (!email && !provider)
throw new Error('Must provide email or SSO provider');
if (!email && !phoneNumber && !provider)
throw new Error('Must provide email or SMS or SSO provider');

const { isCustomDomain } = fetchCachedCustomDomain() || {};
const magic = await constructMagic(isCosmos, chain);

if (email) {
Expand All @@ -334,18 +351,51 @@ export async function startLoginWithMagicLink({
});

return { bearer, address };
} else {
const params = `?redirectTo=${
redirectTo ? encodeURIComponent(redirectTo) : ''
}&chain=${chain || ''}&sso=${provider}`;
await magic.oauth.loginWithRedirect({
provider,
redirectURI: new URL(
'/finishsociallogin' + params,
window.location.origin,
).href,
} else if (provider === WalletSsoSource.Farcaster) {
const bearer = await magic.farcaster.login();

const { address } = await handleSocialLoginCallback({
bearer,
walletSsoSource: WalletSsoSource.Farcaster,
});

Comment on lines +354 to +361
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to strictly follow this? can we reuse the else block logic below? That would redirect to /finish_social_login and then handleSocialLoginCallback is already called there along with some app init settings as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little bit confusing but farcaster is not technically a social login. This is social logins require to use the webhook callback which farcaster does not use since it is decentralized. There is no entity (Like google or whatever) to submit the callback webhook for us.

This basically takes the same path as email login, the only difference is that it needs to call the magic farcaster login method

return { bearer, address };
} else if (phoneNumber) {
const bearer = await magic.auth.loginWithSMS({
phoneNumber,
showUI: true,
});

const { address } = await handleSocialLoginCallback({
bearer,
walletSsoSource: WalletSsoSource.SMS,
});

return { bearer, address };
} else {
localStorage.setItem('magic_provider', provider!);
localStorage.setItem('magic_chain', chain!);
localStorage.setItem('magic_redirect_to', window.location.href);

if (isCustomDomain) {
const redirectTo = document.location.pathname + document.location.search;
const params = `?redirectTo=${
redirectTo ? encodeURIComponent(redirectTo) : ''
}&chain=${chain || ''}&sso=${provider}`;
await magic.oauth.loginWithRedirect({
provider,
redirectURI: new URL(
'/finishsociallogin' + params,
window.location.origin,
).href,
});
} else {
await magic.oauth2.loginWithRedirect({
provider,
redirectURI: new URL('/finishsociallogin', window.location.origin).href,
});
}

// magic should redirect away from this page, but we return after 5 sec if it hasn't
await new Promise<void>((resolve) => setTimeout(() => resolve(), 5000));
const info = await magic.user.getInfo();
Expand Down Expand Up @@ -404,12 +454,15 @@ export async function handleSocialLoginCallback({
}
const isCosmos = desiredChain?.base === ChainBase.CosmosSDK;
const magic = await constructMagic(isCosmos, desiredChain?.id);
const isEmail = walletSsoSource === WalletSsoSource.Email;

// Code up to this line might run multiple times because of extra calls to useEffect().
// Those runs will be rejected because getRedirectResult purges the browser search param.
let profileMetadata, magicAddress;
if (isEmail) {
if (
walletSsoSource === WalletSsoSource.Email ||
walletSsoSource === WalletSsoSource.Farcaster ||
walletSsoSource === WalletSsoSource.SMS
) {
const metadata = await magic.user.getMetadata();
profileMetadata = { username: null };

Expand All @@ -423,7 +476,7 @@ export async function handleSocialLoginCallback({
magicAddress = utils.getAddress(metadata.publicAddress);
}
} else {
const result = await magic.oauth.getRedirectResult();
const result = await magic.oauth2.getRedirectResult();

if (!bearer) {
console.log('No bearer token found in magic redirect result');
Expand All @@ -444,7 +497,8 @@ export async function handleSocialLoginCallback({
try {
// Sign a session
if (isCosmos && desiredChain) {
const signer = { signMessage: magic.cosmos.sign };
// eslint-disable-next-line
const signer = { signMessage: (magic as unknown as any).cosmos.sign };
const prefix = app.chain?.meta?.bech32_prefix || 'cosmos';
const canvasChainId = chainBaseToCanvasChainId(
ChainBase.CosmosSDK,
Expand Down
24 changes: 16 additions & 8 deletions packages/commonwealth/client/scripts/helpers/index.tsx
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes to this file were necessary to fix:
https://github.com/hicommonwealth/commonwealth/actions/runs/11843912133/job/33005933192?pr=9913

The issue was that login.ts now relies on the root entrypoint, causing some dependency issue. By passing in the app to these helpers instead of calling it directly, fixes this issue

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import { TopicWeightedVoting } from '@hicommonwealth/schemas';
import BigNumber from 'bignumber.js';
import moment from 'moment';
import React from 'react';
import app from 'state';
import Account from '../models/Account';
import { IBlockInfo } from '../models/interfaces';
import { ThreadStage } from '../models/types';
import type { IApp } from '../state/index';

export async function sleep(msec) {
return new Promise((resolve) => setTimeout(resolve, msec));
Expand All @@ -26,7 +27,11 @@ export function threadStageToLabel(stage: string) {
}
}

export function isDefaultStage(stage: string, customStages?: string[]) {
export function isDefaultStage(
app: IApp,
stage: string,
customStages?: string[],
) {
return (
stage === ThreadStage.Discussion ||
stage ===
Expand Down Expand Up @@ -174,16 +179,19 @@ export function renderMultilineText(text: string) {
* blocknum helpers
*/

export function blocknumToTime(blocknum: number): moment.Moment {
const currentBlocknum = app.chain.block.height;
const blocktime = app.chain.block.duration;
const lastBlockTime: moment.Moment = app.chain.block.lastTime.clone();
export function blocknumToTime(
block: IBlockInfo,
blocknum: number,
): moment.Moment {
const currentBlocknum = block.height;
const blocktime = block.duration;
const lastBlockTime: moment.Moment = block.lastTime.clone();
return lastBlockTime.add((blocknum - currentBlocknum) * blocktime, 'seconds');
}

export function blocknumToDuration(blocknum: number) {
export function blocknumToDuration(block: IBlockInfo, blocknum: number) {
return moment
.duration(blocknumToTime(blocknum).diff(moment()))
.duration(blocknumToTime(block, blocknum).diff(moment()))
.asMilliseconds();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,18 @@ export const AUTH_TYPES: AuthTypesList = {
},
label: 'Email',
},
SMS: {
icon: {
name: 'SMS',
isCustom: true,
},
label: 'SMS',
},
farcaster: {
icon: {
name: 'farcaster',
isCustom: true,
},
label: 'Farcaster',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ export type AuthSSOs =
| 'x'
| 'github'
| 'apple'
| 'email';
| 'email'
| 'farcaster'
| 'SMS';
export type CosmosWallets = 'keplr' | 'leap';
export type SubstrateWallets = 'polkadot';
export type SolanaWallets = 'phantom';
Expand Down
Loading
Loading