Skip to content

Commit

Permalink
[fcl-wc] add pairing modal override and sessionRequestHook (#1411)
Browse files Browse the repository at this point in the history
* PKG -- [fcl-wc] update fetch wc wallets to use api/v3

* PKG -- [fcl-wc] add function validation to sessionRequest hook

* PKG -- [fcl-wc] open deeplink window before async connect

* PKG -- [fcl-wc] requestHook on mobile for sessions

* PKG -- [fcl-wc] update sessionRequest hool with session/pairing and uri

* PKG -- [fcl-wc] update sclient to singleton, refactor new session

* PKG -- [fcl-wc] add pairing modal overide

* PKG -- [fcl-wc] add method to wcRequestHook, update naming and constants

* PKG -- [fcl-wc] update pairing modal override

* PKG -- [fcl-wc] remove windowRef and use window directly on authn

* PKG -- [fcl-wc] add open deeplink on mobile session request
  • Loading branch information
Greg Santos authored Sep 15, 2022
1 parent 15d7722 commit 3c7a1bd
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 88 deletions.
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

0 comments on commit 3c7a1bd

Please sign in to comment.