diff --git a/landing/siwe.react.js b/landing/siwe.react.js
index ca44e949e..a1d5d1127 100644
--- a/landing/siwe.react.js
+++ b/landing/siwe.react.js
@@ -1,181 +1,189 @@
// @flow
import {
useConnectModal,
RainbowKitProvider,
darkTheme,
useModalState,
connectorsForWallets,
} from '@rainbow-me/rainbowkit';
import '@rainbow-me/rainbowkit/styles.css';
import {
injectedWallet,
rainbowWallet,
metaMaskWallet,
walletConnectWallet,
// eslint-disable-next-line import/extensions
} from '@rainbow-me/rainbowkit/wallets';
import invariant from 'invariant';
import _merge from 'lodash/fp/merge.js';
import * as React from 'react';
import { useAccount, useSigner, WagmiConfig } from 'wagmi';
import ConnectedWalletInfo from 'lib/components/connected-wallet-info.react.js';
import type { SIWEWebViewMessage } from 'lib/types/siwe-types.js';
import {
getSIWEStatementForPublicKey,
siweMessageSigningExplanationStatements,
createSIWEMessage,
} from 'lib/utils/siwe-utils.js';
import {
WagmiENSCacheProvider,
configureWagmiChains,
createWagmiClient,
} from 'lib/utils/wagmi-utils.js';
import { SIWEContext } from './siwe-context.js';
import css from './siwe.css';
import { useMonitorForWalletConnectModal } from './walletconnect-hooks.js';
const projectId = process.env.COMM_WALLETCONNECT_KEY;
const { chains, provider } = configureWagmiChains(process.env.COMM_ALCHEMY_KEY);
const connectors = connectorsForWallets([
{
groupName: 'Recommended',
wallets: [
injectedWallet({ chains }),
rainbowWallet({ chains, projectId }),
metaMaskWallet({ chains, projectId }),
walletConnectWallet({ chains, projectId }),
],
},
]);
const wagmiClient = createWagmiClient({ connectors, provider });
function postMessageToNativeWebView(message: SIWEWebViewMessage) {
window.ReactNativeWebView?.postMessage?.(JSON.stringify(message));
}
function onWalletConnectModalUpdate(update) {
- postMessageToNativeWebView({
- type: 'walletconnect_modal_update',
- ...update,
- });
+ // Conditional is only here for Flow
+ if (update.state === 'closed') {
+ postMessageToNativeWebView({
+ type: 'walletconnect_modal_update',
+ ...update,
+ });
+ } else {
+ postMessageToNativeWebView({
+ type: 'walletconnect_modal_update',
+ ...update,
+ });
+ }
}
async function signInWithEthereum(
address: string,
signer,
nonce: string,
statement: string,
) {
invariant(nonce, 'nonce must be present in signInWithEthereum');
const message = createSIWEMessage(address, statement, nonce);
const signature = await signer.signMessage(message);
postMessageToNativeWebView({
type: 'siwe_success',
address,
message,
signature,
});
}
function SIWE(): React.Node {
const { address } = useAccount();
const { data: signer } = useSigner();
const { siweNonce, siwePrimaryIdentityPublicKey } =
React.useContext(SIWEContext);
const onClick = React.useCallback(() => {
invariant(siweNonce, 'nonce must be present during SIWE attempt');
invariant(
siwePrimaryIdentityPublicKey,
'primaryIdentityPublicKey must be present during SIWE attempt',
);
const statement = getSIWEStatementForPublicKey(
siwePrimaryIdentityPublicKey,
);
signInWithEthereum(address, signer, siweNonce, statement);
}, [address, signer, siweNonce, siwePrimaryIdentityPublicKey]);
const { openConnectModal } = useConnectModal();
const hasNonce = siweNonce !== null && siweNonce !== undefined;
React.useEffect(() => {
if (hasNonce && openConnectModal) {
openConnectModal();
}
}, [hasNonce, openConnectModal]);
const prevConnectModalOpen = React.useRef(false);
const modalState = useModalState();
const closeTimeoutRef = React.useRef();
const { connectModalOpen } = modalState;
React.useEffect(() => {
if (!connectModalOpen && prevConnectModalOpen.current && !signer) {
closeTimeoutRef.current = setTimeout(
() => postMessageToNativeWebView({ type: 'siwe_closed' }),
50,
);
} else if (closeTimeoutRef.current) {
clearTimeout(closeTimeoutRef.current);
closeTimeoutRef.current = undefined;
}
prevConnectModalOpen.current = connectModalOpen;
}, [connectModalOpen, signer]);
useMonitorForWalletConnectModal(onWalletConnectModalUpdate);
if (!hasNonce) {
return (
Unable to proceed: nonce not found.
);
} else if (!signer) {
return null;
} else {
return (
Wallet Connected
{siweMessageSigningExplanationStatements}
By signing up, you agree to our{' '}
Terms of Use &{' '}
Privacy Policy.
Sign in using this wallet
);
}
}
function SIWEWrapper(): React.Node {
const theme = React.useMemo(() => {
return _merge(darkTheme())({
radii: {
modal: 0,
modalMobile: 0,
},
colors: {
modalBackdrop: '#242529',
},
});
}, []);
return (
);
}
export default SIWEWrapper;
diff --git a/landing/walletconnect-hooks.js b/landing/walletconnect-hooks.js
index c612d3754..1a0c8d946 100644
--- a/landing/walletconnect-hooks.js
+++ b/landing/walletconnect-hooks.js
@@ -1,46 +1,119 @@
// @flow
import invariant from 'invariant';
import * as React from 'react';
-type WalletConnectModalUpdate = {
- +state: 'open' | 'closed',
-};
+type WalletConnectModalUpdate =
+ | { +state: 'closed' }
+ | { +state: 'open', +height: number };
function useMonitorForWalletConnectModal(
callback: WalletConnectModalUpdate => mixed,
) {
- const newModalAppeared = React.useCallback(
- mutationList => {
- for (const mutation of mutationList) {
- for (const addedNode of mutation.addedNodes) {
- if (
- addedNode instanceof HTMLElement &&
- addedNode.id === 'walletconnect-wrapper'
- ) {
- callback({ state: 'open' });
- }
- }
- for (const addedNode of mutation.removedNodes) {
- if (
- addedNode instanceof HTMLElement &&
- addedNode.id === 'walletconnect-wrapper'
- ) {
- callback({ state: 'closed' });
- }
+ const [wcShadowRoot, setWCShadowRoot] = React.useState();
+ const [wcResizableContainer, setWCResizableContainer] = React.useState();
+
+ const newShadowRootAppeared = React.useCallback(mutationList => {
+ for (const mutation of mutationList) {
+ for (const addedNode of mutation.addedNodes) {
+ if (
+ addedNode instanceof HTMLElement &&
+ addedNode.localName === 'w3m-modal' &&
+ addedNode.shadowRoot
+ ) {
+ const { shadowRoot } = addedNode;
+ // We actually are looking to track an element inside w3m-modal,
+ // rather than w3m-modal itself. Normally we could pass subtree: true
+ // to observer.observe, but this doesn't appear to work with a "shadow
+ // root", so instead we implement a second-layer MutationObserver once
+ // we see the shadow root.
+ setWCShadowRoot(shadowRoot);
}
}
- },
- [callback],
- );
+ }
+ }, []);
React.useEffect(() => {
- const observer = new MutationObserver(newModalAppeared);
+ const observer = new MutationObserver(newShadowRootAppeared);
invariant(document.body, 'document.body should be set');
observer.observe(document.body, { childList: true });
return () => {
observer.disconnect();
};
- }, [newModalAppeared]);
+ }, [newShadowRootAppeared]);
+
+ const newModalAppeared = React.useCallback(mutationList => {
+ // We pass subtree: true to the MutationObserver that calls this function.
+ // This means we monitor for changes all through the subtree, but if a child
+ // subtree is added, we only get the root of the subtree in addedNodes. As
+ // such we need to recursively scan the subtree to try and find the node
+ // that we're looking for.
+ const nodesToInspect = new Set();
+ const addNodesToInspect = node => {
+ nodesToInspect.add(node);
+ for (const childNode of node.childNodes) {
+ addNodesToInspect(childNode);
+ }
+ };
+ for (const mutation of mutationList) {
+ for (const addedNode of mutation.addedNodes) {
+ addNodesToInspect(addedNode);
+ }
+ }
+ for (const node of nodesToInspect) {
+ if (
+ node instanceof HTMLElement &&
+ node.localName === 'div' &&
+ node.className === 'w3m-container'
+ ) {
+ setWCResizableContainer(node);
+ }
+ }
+ }, []);
+
+ React.useEffect(() => {
+ if (!wcShadowRoot) {
+ return undefined;
+ }
+
+ // We can actually skip the MutationObserver below if by the time the React
+ // state of wcShadowRoot is set, the subtree has already appeared. We store
+ // wcShadowRoot as React state so that we can properly "clean up" the
+ // associated observer below in an effect, but it means we have to deal with
+ // the associated delay in updated React state, which is unpredictable.
+ const modal = wcShadowRoot.getElementById('w3m-modal');
+ if (modal) {
+ const container = wcShadowRoot.querySelector('div.w3m-container');
+ if (container) {
+ setWCResizableContainer(container);
+ return undefined;
+ }
+ }
+
+ const observer = new MutationObserver(newModalAppeared);
+ observer.observe(wcShadowRoot, { childList: true, subtree: true });
+ return () => {
+ observer.disconnect();
+ };
+ }, [wcShadowRoot, newModalAppeared]);
+
+ React.useEffect(() => {
+ if (!wcResizableContainer) {
+ return undefined;
+ }
+ const resizeObserver = new ResizeObserver(entries => {
+ const lastEntry = entries[entries.length - 1];
+ const { height } = lastEntry.contentRect;
+ if (height) {
+ callback({ state: 'open', height });
+ } else {
+ callback({ state: 'closed' });
+ }
+ });
+ resizeObserver.observe(wcResizableContainer);
+ return () => {
+ resizeObserver.disconnect();
+ };
+ }, [wcResizableContainer, callback]);
}
export { useMonitorForWalletConnectModal };
diff --git a/lib/types/siwe-types.js b/lib/types/siwe-types.js
index 23bd199a6..92aad5e2e 100644
--- a/lib/types/siwe-types.js
+++ b/lib/types/siwe-types.js
@@ -1,124 +1,129 @@
// @flow
import type { LogInExtraInfo } from './account-types.js';
import type { SignedIdentityKeysBlob } from './crypto-types.js';
import {
type DeviceTokenUpdateRequest,
type PlatformDetails,
} from './device-types.js';
import { type CalendarQuery } from './entry-types.js';
export type SIWENonceResponse = {
+nonce: string,
};
export type SIWEAuthRequest = {
+message: string,
+signature: string,
+calendarQuery?: ?CalendarQuery,
+deviceTokenUpdateRequest?: ?DeviceTokenUpdateRequest,
+platformDetails: PlatformDetails,
+watchedIDs: $ReadOnlyArray,
+signedIdentityKeysBlob?: ?SignedIdentityKeysBlob,
+initialNotificationsEncryptedMessage?: string,
};
export type SIWEAuthServerCall = {
+message: string,
+signature: string,
...LogInExtraInfo,
};
export type SIWESocialProof = {
+siweMessage: string,
+siweMessageSignature: string,
};
// This is a message that the rendered webpage (landing/siwe.react.js) uses to
// communicate back to the React Native WebView that is rendering it
// (native/account/siwe-panel.react.js)
export type SIWEWebViewMessage =
| {
+type: 'siwe_success',
+address: string,
+message: string,
+signature: string,
}
| {
+type: 'siwe_closed',
}
| {
+type: 'walletconnect_modal_update',
- +state: 'open' | 'closed',
+ +state: 'open',
+ +height: number,
+ }
+ | {
+ +type: 'walletconnect_modal_update',
+ +state: 'closed',
};
export type SIWEMessage = {
// RFC 4501 dns authority that is requesting the signing.
+domain: string,
// Ethereum address performing the signing conformant to capitalization
// encoded checksum specified in EIP-55 where applicable.
+address: string,
// Human-readable ASCII assertion that the user will sign, and it must not
// contain `\n`.
+statement?: string,
// RFC 3986 URI referring to the resource that is the subject of the signing
// (as in the __subject__ of a claim).
+uri: string,
// Current version of the message.
+version: string,
// EIP-155 Chain ID to which the session is bound, and the network where
// Contract Accounts must be resolved.
+chainId: number,
// Randomized token used to prevent replay attacks, at least 8 alphanumeric
// characters.
+nonce: string,
// ISO 8601 datetime string of the current time.
+issuedAt: string,
// ISO 8601 datetime string that, if present, indicates when the signed
// authentication message is no longer valid.
+expirationTime?: string,
// ISO 8601 datetime string that, if present, indicates when the signed
// authentication message will become valid.
+notBefore?: string,
// System-specific identifier that may be used to uniquely refer to the
// sign-in request.
+requestId?: string,
// List of information or references to information the user wishes to have
// resolved as part of authentication by the relying party. They are
// expressed as RFC 3986 URIs separated by `\n- `.
+resources?: $ReadOnlyArray,
// @deprecated
// Signature of the message signed by the wallet.
//
// This field will be removed in future releases, an additional parameter
// was added to the validate function were the signature goes to validate
// the message.
+signature?: string,
// @deprecated
// Type of sign message to be generated.
//
// This field will be removed in future releases and will rely on the
// message version.
+type?: 'Personal signature',
+validate: (signature: string, provider?: any) => Promise,
+toMessage: () => string,
};
export type SIWEResult = {
+address: string,
+message: string,
+signature: string,
};