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
1 change: 1 addition & 0 deletions libs/shared/src/types/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export enum WalletSsoSource {
Twitter = 'twitter',
Apple = 'apple',
Email = 'email',
Farcaster = 'farcaster',
Unknown = 'unknown', // address created after we launched SSO, before we started recording WalletSsoSource
}

Expand Down
9 changes: 9 additions & 0 deletions packages/commonwealth/client/scripts/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { FarcasterExtension } from '@magic-ext/farcaster';
import { OAuthExtension } from '@magic-ext/oauth2';
import { OpenFeatureProvider } from '@openfeature/react-sdk';
import { OpenFeature } from '@openfeature/web-sdk';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import useInitApp from 'hooks/useInitApp';
import { Magic } from 'magic-sdk';
import router from 'navigation/Router';
import React, { StrictMode } from 'react';
import { HelmetProvider } from 'react-helmet-async';
Expand All @@ -18,6 +21,12 @@ import { Mava } from './views/components/Mava';

OpenFeature.setProvider(openFeatureProvider);

// 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!, {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This might not be the best place to put it, but it needs to be instantiated when the app starts. Any better suggestions?

Copy link
Contributor

Choose a reason for hiding this comment

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

not sure but does this cause circular dependency issues? or maybe we could add to packages/commonwealth/client/scripts/controllers/app/login.ts (that needs it - also login.ts is imported in many init flow conditions)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I moved it to login.ts. And yes this is the cause of the test failure issue. Not exactly a circular dependency but more of this Magic constructor calls the DOM, but our tests don't have the DOM so it throws an error.

But basically this is needed because their constructor needs to complete an async request before we can call login. So basically the earlier it is instantiated the better, but still no guarantee that login won't fail when it is called.

extensions: [new FarcasterExtension(), new OAuthExtension()],
});

const App = () => {
const { isLoading } = useInitApp();
const { isAddedToHomeScreen, isMarketingPage, isIOS, isAndroid } =
Expand Down
65 changes: 38 additions & 27 deletions packages/commonwealth/client/scripts/controllers/app/login.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,27 @@
/**
* @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 { OAuthExtension } 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 @@ -34,6 +32,7 @@ import {
import { welcomeOnboardModal } from 'state/ui/modals/welcomeOnboardModal';
import { userStore } from 'state/ui/user';
import { z } from 'zod';
import { defaultMagic } from '../../App';
import Account from '../../models/Account';
import AddressInfo from '../../models/AddressInfo';
import type BlockInfo from '../../models/BlockInfo';
Expand Down Expand Up @@ -287,24 +286,27 @@ 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 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}`,
}),
],
});
}

Expand Down Expand Up @@ -333,12 +335,21 @@ export async function startLoginWithMagicLink({
walletSsoSource: WalletSsoSource.Email,
});

return { bearer, address };
} 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 {
const params = `?redirectTo=${
redirectTo ? encodeURIComponent(redirectTo) : ''
}&chain=${chain || ''}&sso=${provider}`;
await magic.oauth.loginWithRedirect({
await magic.oauth2.loginWithRedirect({
provider,
redirectURI: new URL(
'/finishsociallogin' + params,
Expand Down Expand Up @@ -409,7 +420,7 @@ export async function handleSocialLoginCallback({
// 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 (isEmail || walletSsoSource === WalletSsoSource.Farcaster) {
const metadata = await magic.user.getMetadata();
profileMetadata = { username: null };

Expand All @@ -423,7 +434,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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,11 @@ export const AUTH_TYPES: AuthTypesList = {
},
label: 'Email',
},
farcaster: {
icon: {
name: 'farcaster',
isCustom: true,
},
label: 'Farcaster',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export type AuthSSOs =
| 'x'
| 'github'
| 'apple'
| 'email';
| 'email'
| 'farcaster';
export type CosmosWallets = 'keplr' | 'leap';
export type SubstrateWallets = 'polkadot';
export type SolanaWallets = 'phantom';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import React from 'react';

/* eslint-disable max-len */
/* eslint-disable react/no-multi-comp */

import 'components/component_kit/cw_icon.scss';

import { getClasses } from '../helpers';
import type { CustomIconProps, CustomIconStyleProps } from './types';
import type { CustomIconProps, CustomIconStyleProps, IconProps } from './types';

// ADDING CUSTOM ICONS: INSTRUCTIONS
//
Expand Down Expand Up @@ -215,6 +214,35 @@ export const CWApple = (props: CustomIconProps) => {
);
};

export const CWFarcaster = (props: IconProps) => {
const { componentType, iconSize, ...otherProps } = props;
return (
<svg
width="1000"
height="1000"
viewBox="0 0 1000 1000"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={getClasses<CustomIconStyleProps>({ iconSize }, componentType)}
{...otherProps}
>
<rect width="1000" height="1000" rx="200" fill="#855DCD" />
<path
d="M257.778 155.556H742.222V844.444H671.111V528.889H670.414C662.554 441.677 589.258 373.333 500 373.333C410.742 373.333 337.446 441.677 329.586 528.889H328.889V844.444H257.778V155.556Z"
fill="white"
/>
<path
d="M128.889 253.333L157.778 351.111H182.222V746.667C169.949 746.667 160 756.616 160 768.889V795.556H155.556C143.283 795.556 133.333 805.505 133.333 817.778V844.444H382.222V817.778C382.222 805.505 372.273 795.556 360 795.556H355.556V768.889C355.556 756.616 345.606 746.667 333.333 746.667H306.667V253.333H128.889Z"
fill="white"
/>
<path
d="M675.556 746.667C663.283 746.667 653.333 756.616 653.333 768.889V795.556H648.889C636.616 795.556 626.667 805.505 626.667 817.778V844.444H875.556V817.778C875.556 805.505 865.606 795.556 853.333 795.556H848.889V768.889C848.889 756.616 838.94 746.667 826.667 746.667V351.111H851.111L880 253.333H702.222V746.667H675.556Z"
fill="white"
/>
</svg>
);
};

export const CWMagic = (props: CustomIconProps) => {
const { componentType, iconSize, className, ...otherProps } = props;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ export const customIconLookup = {
coinbase: CustomIcons.CWCoinbase,
x: CustomIcons.CWX, // twitter
apple: CustomIcons.CWApple,
farcaster: CustomIcons.CWFarcaster,
};

export type IconName = keyof typeof iconLookup;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const SSO_OPTIONS: AuthSSOs[] = [
'apple',
'github',
'email',
'farcaster',
] as const;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const AuthButtonsShowcase = () => {
<AuthButton type="discord" />
<AuthButton type="x" />
<AuthButton type="email" />
<AuthButton type="farcaster" />
</div>
<CWText type="h5">Disabled</CWText>
<div className="flex-row">
Expand Down
11 changes: 6 additions & 5 deletions packages/commonwealth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -116,16 +116,17 @@
"@ipld/dag-json": "^10.2.0",
"@keplr-wallet/types": "^0.12.23",
"@keplr-wallet/unit": "^0.12.23",
"@knocklabs/node": "^0.6.13",
"@knocklabs/client": "^0.10.13",
"@knocklabs/node": "^0.6.13",
"@knocklabs/react": "^0.2.15",
"@knocklabs/react-notification-feed": "^0.8.15",
"@lexical/rich-text": "^0.17.0",
"@libp2p/crypto": "^5.0.4",
"@libp2p/peer-id": "^5.0.4",
"@magic-ext/cosmos": "^12.1.3",
"@magic-ext/oauth": "^11.1.1",
"@magic-sdk/admin": "^2.4.0",
"@magic-ext/cosmos": "^12.4.0",
"@magic-ext/farcaster": "^0.16.0",
"@magic-ext/oauth2": "^9.16.0",
"@magic-sdk/admin": "^2.4.1",
"@metamask/detect-provider": "^2.0.0",
"@metamask/eth-sig-util": "^4.0.0",
"@mui/base": "5.0.0-beta.5",
Expand Down Expand Up @@ -202,7 +203,7 @@
"lexical": "^0.17.0",
"lodash": "^4.17.21",
"long": "^5.2.3",
"magic-sdk": "^17.1.4",
"magic-sdk": "^28.17.0",
"marked": "^11.0.0",
"marked-footnote": "^1.2.2",
"marked-smartypants": "1.1.5",
Expand Down
Loading
Loading