Skip to content
This repository has been archived by the owner on Jul 31, 2023. It is now read-only.

Commit

Permalink
Merge pull request #60 from ZYJLiu/main
Browse files Browse the repository at this point in the history
Update Magic Connect Connector to use Magic Connect's Modal UI
  • Loading branch information
Royal-lobster authored Mar 28, 2023
2 parents ef62336 + 58b39f3 commit 26be453
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 109 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-peaches-call.md
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
223 changes: 118 additions & 105 deletions src/lib/connectors/magicConnectConnector.ts
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');
};
}
5 changes: 1 addition & 4 deletions src/lib/connectors/magicConnector.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { ConnectExtension } from '@magic-ext/connect';
import { OAuthExtension, OAuthProvider } from '@magic-ext/oauth';
import { InstanceWithExtensions, SDKBase } from '@magic-sdk/provider';
import { RPCProviderModule } from '@magic-sdk/provider/dist/types/modules/rpc-provider';
Expand Down Expand Up @@ -118,7 +117,5 @@ export abstract class MagicConnector extends Connector {
await magic.user.logout();
}

abstract getMagicSDK():
| InstanceWithExtensions<SDKBase, OAuthExtension[]>
| InstanceWithExtensions<SDKBase, ConnectExtension[]>;
abstract getMagicSDK(): InstanceWithExtensions<SDKBase, OAuthExtension[]>;
}

0 comments on commit 26be453

Please sign in to comment.