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

[fcl-wc] add pairing modal override and sessionRequestHook #1411

Merged
merged 11 commits into from
Sep 15, 2022
41 changes: 41 additions & 0 deletions .changeset/lazy-poets-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
"@onflow/fcl-wc": patch
---

Adds additional options to `init` for `pairingModalOverride` and `wcRequestHook`

```js
import * as fcl from '@onflow/fcl'
import { init } from '@onflow/fcl-wc'
// example using pairing data from wcRequestHook and providing a custom pairing modal
const { FclWcServicePlugin, client } = await init({
projectId: PROJECT_ID,
metadata: PROJECT_METADATA,
includeBaseWC: false,
wallets: [],
wcRequestHook: (data: WcRequestData) => {
const peerMetadata = data?.pairing?.peerMetadata
setSessionRequestData(peerMetadata)
setShowRequestModal(true)
},
pairingModalOverride: (uri: string = '', rejectPairingRequest: () => void) => {
openCustomPairingModal(uri)
// call rejectPairingRequest() to manually reject pairing request from client
}
})

fcl.pluginRegistry.add(FclWcServicePlugin)

```

```ts

interface WcRequestData {
type: string // 'session_request' | 'pairing_request'
session: SessionTypes.Struct | undefined // https://www.npmjs.com/package/@walletconnect/types
pairing: PairingTypes.Struct | undefined // https://www.npmjs.com/package/@walletconnect/types
method: string // "flow_authn" | "flow_authz" | "flow_user_sign"
uri: string | undefined
}

```
10 changes: 10 additions & 0 deletions packages/fcl-wc/src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const FLOW_METHODS = {
FLOW_AUTHN: "flow_authn",
FLOW_AUTHZ: "flow_authz",
FLOW_USER_SIGN: "flow_user_sign",
}

export const REQUEST_TYPES = {
SESSION: "session_request",
PAIRING: "pairing_request",
}
17 changes: 11 additions & 6 deletions packages/fcl-wc/src/fcl-wc.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
import * as fcl from "@onflow/fcl"
import SignClient from "@walletconnect/sign-client"
import {makeServicePlugin} from "./service"
import {invariant} from "@onflow/util-invariant"
import {LEVELS, log} from "@onflow/util-logger"
import * as fcl from "@onflow/fcl"
export {getSdkError} from "@walletconnect/utils"
import {setConfiguredNetwork} from "./utils"

const DEFAULT_RELAY_URL = "wss://relay.walletconnect.com"
const DEFAULT_LOGGER = "debug"
let client = null

const initClient = async ({projectId, metadata}) => {
invariant(
projectId != null,
"FCL Wallet Connect Error: WalletConnect projectId is required"
)
try {
return SignClient.init({
client = await SignClient.init({
logger: DEFAULT_LOGGER,
relayUrl: DEFAULT_RELAY_URL,
projectId: projectId,
metadata: metadata,
})
return client
} catch (error) {
log({
title: `${error.name} fcl-wc Init Client`,
Expand All @@ -35,14 +37,17 @@ export const init = async ({
projectId,
metadata,
includeBaseWC = false,
sessionRequestHook = null,
wcRequestHook = null,
pairingModalOverride = null,
wallets = [],
} = {}) => {
const client = await initClient({projectId, metadata})
await setConfiguredNetwork()
const FclWcServicePlugin = await makeServicePlugin(client, {
const _client = client ?? (await initClient({projectId, metadata}))
const FclWcServicePlugin = await makeServicePlugin(_client, {
projectId,
includeBaseWC,
sessionRequestHook,
wcRequestHook,
pairingModalOverride,
wallets,
})
fcl.discovery.authn.update()
Expand Down
197 changes: 120 additions & 77 deletions packages/fcl-wc/src/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import QRCodeModal from "@walletconnect/qrcode-modal"
import {invariant} from "@onflow/util-invariant"
import {log, LEVELS} from "@onflow/util-logger"
import {fetchFlowWallets, isMobile, CONFIGURED_NETWORK} from "./utils"
import {FLOW_METHODS, REQUEST_TYPES} from "./constants"

export const makeServicePlugin = async (client, opts = {}) => ({
name: "fcl-plugin-service-walletconnect",
Expand All @@ -11,60 +12,55 @@ export const makeServicePlugin = async (client, opts = {}) => ({
serviceStrategy: {method: "WC/RPC", exec: makeExec(client, opts)},
})

const makeExec = (client, {sessionRequestHook}) => {
const makeExec = (client, {wcRequestHook, pairingModalOverride}) => {
return ({service, body, opts}) => {
return new Promise(async (resolve, reject) => {
invariant(client, "WalletConnect is not initialized")
let session
const onResponse = resp => {
try {
if (typeof resp !== "object") return

switch (resp.status) {
case "APPROVED":
resolve(resp.data)
break

case "DECLINED":
reject(`Declined: ${resp.reason || "No reason supplied"}`)
break

case "REDIRECT":
resolve(resp)
break

default:
reject(`Declined: No reason supplied`)
break
}
} catch (error) {
log({
title: `${error.name} "WC/RPC onResponse error"`,
message: error.message,
level: LEVELS.error,
})
throw error
}
}
let session, pairing
const appLink = service.uid
const method = service.endpoint

const onClose = () => {
reject(`Declined: Externally Halted`)
const pairings = client.pairing.getAll({active: true})
if (pairings.length > 0) {
pairing = pairings?.find(p => p.peerMetadata?.url === service.uid)
}

if (client.session.length) {
if (client.session.length > 0) {
const lastKeyIndex = client.session.keys.length - 1
session = client.session.get(client.session.keys.at(lastKeyIndex))
}

if (session == null) {
const pairings = client.pairing.getAll({active: true})
const pairing = pairings?.find(p => p.peerMetadata.url === service.uid)
if (session) {
if (isMobile()) window.location.href = appLink
log({
title: "WalletConnect Request",
message: `
Check your ${
session?.peer?.metadata?.name || pairing?.peerMetadata?.name
} Mobile Wallet to Approve/Reject this request
`,
level: LEVELS.warn,
})
if (wcRequestHook && wcRequestHook instanceof Function) {
wcRequestHook({
type: REQUEST_TYPES.SESSION,
session,
pairing,
method,
uri: null,
})
}
}

session = await connectWc(onClose, {
service,
if (session == null) {
session = await connectWc({
onClose,
appLink,
client,
method,
pairing,
sessionRequestHook,
wcRequestHook,
pairingModalOverride,
})
}

Expand All @@ -74,7 +70,6 @@ const makeExec = (client, {sessionRequestHook}) => {
.filter(account => account.startsWith("flow:"))[0]
.split(":")

const method = service.endpoint
const chainId = `${namespace}:${reference}`
const addr = address
const data = JSON.stringify({...body, addr})
Expand All @@ -89,67 +84,116 @@ const makeExec = (client, {sessionRequestHook}) => {
},
})
onResponse(result)
} catch (e) {
} catch (error) {
log({
title: `${e.name} Error on WalletConnect client ${method} request`,
message: e.message,
title: `${error.name} Error on WalletConnect client ${method} request`,
message: error.message,
level: LEVELS.error,
})
reject(`Declined: Externally Halted`)
}

function onResponse(resp) {
try {
if (typeof resp !== "object") return

switch (resp.status) {
case "APPROVED":
resolve(resp.data)
break

case "DECLINED":
reject(`Declined: ${resp.reason || "No reason supplied"}`)
break

case "REDIRECT":
resolve(resp)
break

default:
reject(`Declined: No reason supplied`)
break
}
} catch (error) {
log({
title: `${error.name} "WC/RPC onResponse error"`,
message: error.message,
level: LEVELS.error,
})
throw error
}
}

function onClose() {
reject(`Declined: Externally Halted`)
}
})
}
}

async function connectWc(
async function connectWc({
onClose,
{service, client, pairing, sessionRequestHook}
) {
appLink,
client,
method,
pairing,
wcRequestHook,
pairingModalOverride,
}) {
try {
const requiredNamespaces = {
flow: {
methods: ["flow_authn", "flow_authz", "flow_user_sign"],
methods: [
FLOW_METHODS.FLOW_AUTHN,
FLOW_METHODS.FLOW_AUTHZ,
FLOW_METHODS.FLOW_USER_SIGN,
],
chains: [`flow:${CONFIGURED_NETWORK}`],
events: ["chainChanged", "accountsChanged"],
},
}

const {uri, approval} = await client.connect({
pairingTopic: pairing?.topic,
requiredNamespaces,
})

const appLink = service.uid || pairing?.peerMetadata?.url

if (!isMobile() && !pairing) {
QRCodeModal.open(uri, () => {
onClose()
})
} else if (!isMobile() && pairing) {
log({
title: "WalletConnect Session request",
message: `
${pairing.peerMetadata.name}
Pairing exists, Approve Session in your Mobile Wallet
`,
level: LEVELS.warn,
var _uri = uri

if (wcRequestHook && wcRequestHook instanceof Function) {
wcRequestHook({
type: REQUEST_TYPES.PAIRING,
session,
pairing,
method,
uri,
})
sessionRequestHook && sessionRequestHook(pairing.peerMetadata)
} else {
}

if (isMobile()) {
const queryString = new URLSearchParams({uri: uri}).toString()
let url = pairing == null ? appLink + "?" + queryString : appLink
window.open(url, "blank").focus()
window.location.href = url
} else if (!pairing && uri) {
if (!pairingModalOverride) {
QRCodeModal.open(uri, () => {
onClose()
})
} else {
pairingModalOverride(uri, onClose)
}
}

const session = await approval()
return session
} catch (e) {
} catch (error) {
log({
title: `${e.name} "Error establishing Walletconnect session"`,
message: e.message,
title: `Error establishing Walletconnect session`,
message: `${error.message}
uri: ${_uri}
`,
level: LEVELS.error,
})
throw e
onClose()
throw error
} finally {
QRCodeModal.close()
}
Expand All @@ -176,10 +220,9 @@ const baseWalletConnectService = includeBaseWC => {
}
}

async function makeWcServices({includeBaseWC, wallets}) {
async function makeWcServices({projectId, includeBaseWC, wallets}) {
const wcBaseService = baseWalletConnectService(includeBaseWC)
const flowWcWalletServices = await fetchFlowWallets()
const injectedWalletsServices =
CONFIGURED_NETWORK === "testnet" ? wallets : []
return [wcBaseService, ...flowWcWalletServices, ...injectedWalletsServices]
const flowWcWalletServices = (await fetchFlowWallets(projectId)) ?? []
const injectedWalletServices = CONFIGURED_NETWORK === "testnet" ? wallets : []
return [wcBaseService, ...flowWcWalletServices, ...injectedWalletServices]
}
Loading