This repository has been archived by the owner on Jul 31, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 40
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #60 from ZYJLiu/main
Update Magic Connect Connector to use Magic Connect's Modal UI
- Loading branch information
Showing
3 changed files
with
124 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@everipedia/wagmi-magic-connector": patch | ||
--- | ||
|
||
Update Magic Connect Connector to use Magic Connect's Modal UI |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,147 +1,160 @@ | ||
import { ConnectExtension } from '@magic-ext/connect'; | ||
import { Magic } from 'magic-sdk'; | ||
import { | ||
InstanceWithExtensions, | ||
MagicSDKAdditionalConfiguration, | ||
SDKBase, | ||
} from '@magic-sdk/provider'; | ||
import { ConnectExtension } from '@magic-ext/connect'; | ||
import { | ||
Address, | ||
Chain, | ||
normalizeChainId, | ||
UserRejectedRequestError, | ||
Connector, | ||
} from '@wagmi/core'; | ||
import { Magic } from 'magic-sdk'; | ||
|
||
import { MagicConnector, MagicOptions } from './magicConnector'; | ||
|
||
interface MagicConnectOptions extends MagicOptions { | ||
import { AbstractProvider } from 'web3-core'; | ||
import { RPCProviderModule } from '@magic-sdk/provider/dist/types/modules/rpc-provider'; | ||
import { ethers, Signer } from 'ethers'; | ||
import { getAddress } from 'ethers/lib/utils.js'; | ||
|
||
// Define the interface for MagicConnector options | ||
export interface MagicConnectorOptions { | ||
apiKey: string; | ||
magicSdkConfiguration?: MagicSDKAdditionalConfiguration; | ||
} | ||
|
||
const CONNECT_TIME_KEY = 'wagmi-magic-connector.connect.time'; | ||
const CONNECT_DURATION = 604800000; // 7 days in milliseconds | ||
|
||
export class MagicConnectConnector extends MagicConnector { | ||
magicSDK?: InstanceWithExtensions<SDKBase, ConnectExtension[]>; | ||
// MagicConnectConnector class extends the base wagmi Connector class | ||
export class MagicConnectConnector extends Connector { | ||
readonly id = 'magic'; | ||
readonly name = 'Magic'; | ||
readonly ready = true; | ||
provider: RPCProviderModule & AbstractProvider; | ||
magic: InstanceWithExtensions<SDKBase, ConnectExtension[]>; | ||
|
||
// Constructor initializes the Magic instance | ||
// allows connectUI modal to display faster when connect method is called | ||
constructor(config: { chains?: Chain[]; options: MagicConnectorOptions }) { | ||
super(config); | ||
this.initializeMagicInstance(); | ||
} | ||
|
||
magicSdkConfiguration: MagicConnectOptions['magicSdkConfiguration']; | ||
// Private method to initialize the Magic instance | ||
private initializeMagicInstance() { | ||
const { apiKey, magicSdkConfiguration } = this.options; | ||
if (typeof window !== 'undefined') { | ||
this.magic = new Magic(apiKey, { | ||
...magicSdkConfiguration, | ||
extensions: [new ConnectExtension()], | ||
}); | ||
|
||
constructor(config: { chains?: Chain[]; options: MagicConnectOptions }) { | ||
super(config); | ||
this.magicSdkConfiguration = config.options.magicSdkConfiguration; | ||
this.provider = this.magic.rpcProvider; | ||
console.log('initializeMagicInstance', this.magic); | ||
} | ||
} | ||
|
||
// Connect method attempts to connects to wallet using Magic Connect modal | ||
async connect() { | ||
if (!this.magicOptions.apiKey) | ||
throw new Error('Magic API Key is not provided.'); | ||
try { | ||
await this.magic.wallet.connectWithUI(); | ||
const provider = await this.getProvider(); | ||
const chainId = await this.getChainId(); | ||
|
||
this.registerProviderEventListeners(provider); | ||
|
||
if (provider.on) { | ||
provider.on('accountsChanged', this.onAccountsChanged); | ||
provider.on('chainChanged', this.onChainChanged); | ||
provider.on('disconnect', this.onDisconnect); | ||
} | ||
|
||
// Check if there is a user logged in | ||
const isAuthenticated = await this.isAuthorized(); | ||
|
||
// Check if we have a chainId, in case of error just assign 0 for legacy | ||
let chainId: number; | ||
try { | ||
chainId = await this.getChainId(); | ||
} catch (e) { | ||
chainId = 0; | ||
} | ||
|
||
// if there is a user logged in, return the user | ||
if (isAuthenticated) { | ||
return { | ||
provider, | ||
chain: { | ||
id: chainId, | ||
unsupported: false, | ||
}, | ||
account: await this.getAccount(), | ||
}; | ||
} | ||
|
||
// open the modal and process the magic login steps | ||
if (!this.isModalOpen) { | ||
const output = await this.getUserDetailsByForm(false, true, []); | ||
const magic = this.getMagicSDK(); | ||
|
||
// LOGIN WITH MAGIC LINK WITH EMAIL | ||
if (output.email) { | ||
await magic.wallet.connectWithUI(); | ||
|
||
const signer = await this.getSigner(); | ||
let account = (await signer.getAddress()) as Address; | ||
if (!account.startsWith('0x')) account = `0x${account}`; | ||
|
||
// As we have no way to know if a user is connected to Magic Connect we store a connect timestamp | ||
// in local storage | ||
window.localStorage.setItem( | ||
CONNECT_TIME_KEY, | ||
String(new Date().getTime()) | ||
); | ||
|
||
return { | ||
account, | ||
chain: { | ||
id: chainId, | ||
unsupported: false, | ||
}, | ||
provider, | ||
}; | ||
} | ||
} | ||
throw new UserRejectedRequestError('User rejected request'); | ||
const account = await this.getAccount(); | ||
|
||
return { | ||
account, | ||
chain: { | ||
id: chainId, | ||
unsupported: false, | ||
}, | ||
provider, | ||
}; | ||
} catch (error) { | ||
throw new UserRejectedRequestError('Something went wrong'); | ||
throw new UserRejectedRequestError(error); | ||
} | ||
} | ||
|
||
// Private method to register event listeners for the provider | ||
private registerProviderEventListeners( | ||
provider: RPCProviderModule & AbstractProvider | ||
) { | ||
if (provider.on) { | ||
provider.on('accountsChanged', this.onAccountsChanged); | ||
provider.on('chainChanged', this.onChainChanged); | ||
provider.on('disconnect', this.onDisconnect); | ||
} | ||
} | ||
|
||
// Disconnect method attempts to disconnect wallet from Magic | ||
async disconnect(): Promise<void> { | ||
try { | ||
await this.magic.wallet.disconnect(); | ||
this.emit('disconnect'); | ||
} catch (error) { | ||
console.error('Error disconnecting from Magic SDK:', error); | ||
} | ||
} | ||
|
||
// Get connected wallet address | ||
async getAccount(): Promise<Address> { | ||
const signer = await this.getSigner(); | ||
const account = await signer.getAddress(); | ||
return getAddress(account); | ||
} | ||
|
||
// Get chain ID | ||
async getChainId(): Promise<number> { | ||
const networkOptions = this.magicSdkConfiguration?.network; | ||
const networkOptions = this.options.magicSdkConfiguration?.network; | ||
if (typeof networkOptions === 'object') { | ||
const chainID = networkOptions.chainId; | ||
if (chainID) { | ||
return normalizeChainId(chainID); | ||
} | ||
if (chainID) return normalizeChainId(chainID); | ||
} | ||
throw new Error('Chain ID is not defined'); | ||
} | ||
|
||
getMagicSDK(): InstanceWithExtensions<SDKBase, ConnectExtension[]> { | ||
if (!this.magicSDK) { | ||
this.magicSDK = new Magic(this.magicOptions.apiKey, { | ||
...this.magicSdkConfiguration, | ||
extensions: [new ConnectExtension()], | ||
}); | ||
// Get the Magic Instance provider | ||
async getProvider() { | ||
if (!this.provider) { | ||
this.provider = this.magic.rpcProvider; | ||
} | ||
return this.magicSDK; | ||
return this.provider; | ||
} | ||
|
||
// Get the Magic Instance signer | ||
async getSigner(): Promise<Signer> { | ||
const provider = new ethers.providers.Web3Provider( | ||
await this.getProvider() | ||
); | ||
return provider.getSigner(); | ||
} | ||
|
||
// Overrides isAuthorized because Connect opens overlay whenever we interact with one of its methods. | ||
// Moreover, there is currently no proper way to know if a user is currently logged in to Magic Connect. | ||
// So we use a local storage state to handle this information. | ||
// TODO Once connect API grows, integrate it | ||
// Autoconnect if account is available | ||
async isAuthorized() { | ||
if (localStorage.getItem(CONNECT_TIME_KEY) === null) { | ||
try { | ||
const walletInfo = await this.magic.wallet.getInfo(); | ||
return !!walletInfo; | ||
} catch { | ||
return false; | ||
} | ||
return ( | ||
parseInt(window.localStorage.getItem(CONNECT_TIME_KEY)) + | ||
CONNECT_DURATION > | ||
new Date().getTime() | ||
); | ||
} | ||
|
||
// Overrides disconnect because there is currently no proper way to disconnect a user from Magic | ||
// Connect. | ||
// So we use a local storage state to handle this information. | ||
async disconnect(): Promise<void> { | ||
window.localStorage.removeItem(CONNECT_TIME_KEY); | ||
} | ||
// Event handler for accountsChanged event | ||
onAccountsChanged = (accounts: string[]): void => { | ||
if (accounts.length === 0) this.emit('disconnect'); | ||
else this.emit('change', { account: getAddress(accounts[0]) }); | ||
}; | ||
|
||
// Event handler for chainChanged event | ||
onChainChanged = (chainId: string | number): void => { | ||
const id = normalizeChainId(chainId); | ||
const unsupported = this.isChainUnsupported(id); | ||
this.emit('change', { chain: { id, unsupported } }); | ||
}; | ||
|
||
// Event handler for disconnect event | ||
onDisconnect = (): void => { | ||
this.emit('disconnect'); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters