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

feat(unlock-app): Support ENS in checkout builder for Referrer field #14834

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { loadStripe } from '@stripe/stripe-js'
import { useSelector } from '@xstate/react'
import { PoweredByUnlock } from '../../PoweredByUnlock'
import { Pricing } from '../../Lock'
import { getReferrer, lockTickerSymbol } from '~/utils/checkoutLockUtils'
import { lockTickerSymbol } from '~/utils/checkoutLockUtils'
import { Lock } from '~/unlockTypes'
import { RiErrorWarningFill as ErrorIcon } from 'react-icons/ri'
import { usePurchase } from '~/hooks/usePurchase'
Expand All @@ -22,6 +22,7 @@ import { useGetLockSettings } from '~/hooks/useLockSettings'
import { getCurrencySymbol } from '~/utils/currency'
import Disconnect from '../Disconnect'
import { ToastHelper } from '~/components/helpers/toast.helper'
import { useGetReferrers } from '~/hooks/useGetReferrers'

interface Props {
checkoutService: CheckoutService
Expand Down Expand Up @@ -171,6 +172,11 @@ export function ConfirmCard({ checkoutService, onConfirmed, onError }: Props) {
network: lock!.network,
purchaseData: purchaseData || [],
})
const { data: referrers } = useGetReferrers(
recipients,
paywallConfig,
lockAddress
)

const { mutateAsync: capturePayment } = useCapturePayment({
network: lock!.network,
Expand All @@ -191,10 +197,6 @@ export function ConfirmCard({ checkoutService, onConfirmed, onError }: Props) {
const onConfirmCard = async () => {
setIsConfirming(true)
try {
const referrers: string[] = recipients.map((recipient) => {
return getReferrer(recipient, paywallConfig, lockAddress)
})

const stripeIntent = await createPurchaseIntent({
pricing: totalPricing!.total * 100, //
// @ts-expect-error - generated types don't narrow down to the right type
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Fragment, useCallback, useState } from 'react'
import { useSelector } from '@xstate/react'
import { PoweredByUnlock } from '../../PoweredByUnlock'
import { Pricing } from '../../Lock'
import { getReferrer, lockTickerSymbol } from '~/utils/checkoutLockUtils'
import { lockTickerSymbol } from '~/utils/checkoutLockUtils'
import { Lock } from '~/unlockTypes'
import { RiErrorWarningFill as ErrorIcon } from 'react-icons/ri'
import { usePricing } from '~/hooks/usePricing'
Expand All @@ -19,6 +19,7 @@ import { TransactionAnimation } from '../../Shell'
import { config } from '~/config/app'
import { useGetTokenIdForOwner } from '~/hooks/useGetTokenIdForOwner'
import Disconnect from '../Disconnect'
import { useGetReferrers } from '~/hooks/useGetReferrers'

interface Props {
checkoutService: CheckoutService
Expand Down Expand Up @@ -46,7 +47,6 @@ export function ConfirmCrossmint({
useSelector(checkoutService, (state) => state.context)
const [isConfirming, setIsConfirming] = useState(false)
const [quote, setQuote] = useState<CrossmintQuote | null>(null)

const crossmintEnv = config.env === 'prod' ? 'production' : 'staging'

const {
Expand All @@ -73,6 +73,12 @@ export function ConfirmCrossmint({
environment: crossmintEnv,
})

const { data: referrers } = useGetReferrers(
recipients,
paywallConfig,
lock!.address
)

// Handling payment events
const onCrossmintPaymentEvent = useCallback(
(paymentEvent: any) => {
Expand Down Expand Up @@ -133,10 +139,6 @@ export function ConfirmCrossmint({
crossmintLoading ||
(!tokenId && renew))

const referrers: string[] = recipients.map((recipient) => {
return getReferrer(recipient, paywallConfig, lock!.address)
})

const values = pricingData
? [
ethers.parseUnits(
Expand Down Expand Up @@ -170,7 +172,7 @@ export function ConfirmCrossmint({
crossmintConfig.mintConfig = {
totalPrice: pricingData?.total.toString(),
_values: values,
_referrers: referrers,
_referrers: referrers?.[0],
_keyManagers: keyManagers || recipients,
_data: purchaseData,
}
Expand All @@ -180,7 +182,7 @@ export function ConfirmCrossmint({
totalPrice: pricingData?.total.toString(),
_tokenId: tokenId,
_value: values[0],
_referrer: referrers[0],
_referrer: referrers?.[0],
_data: purchaseData ? purchaseData[0] : '',
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getAccountTokenBalance } from '~/hooks/useAccount'
import { useSelector } from '@xstate/react'
import { useWeb3Service } from '~/utils/withWeb3Service'
import { Pricing } from '../../Lock'
import { getReferrer, lockTickerSymbol } from '~/utils/checkoutLockUtils'
import { getReferrers, lockTickerSymbol } from '~/utils/checkoutLockUtils'
import { Lock } from '~/unlockTypes'
import ReCaptcha from 'react-google-recaptcha'
import { RiErrorWarningFill as ErrorIcon } from 'react-icons/ri'
Expand Down Expand Up @@ -146,10 +146,6 @@ export function ConfirmCrypto({
pricingData?.prices.map((item) => item.amount.toString()) ||
new Array(recipients!.length).fill(keyPrice)

const referrers: string[] = recipients.map((recipient) => {
return getReferrer(recipient, paywallConfig, lockAddress)
})

const onErrorCallback = (error: Error | null, hash: string | null) => {
setIsConfirming(false)
if (error) {
Expand All @@ -165,11 +161,16 @@ export function ConfirmCrypto({

const walletService = await getWalletService(lockNetwork)
if (renew) {
const referrers = await getReferrers(
[account!],
paywallConfig,
lockAddress
)
await walletService.extendKey(
{
lockAddress,
owner: recipients?.[0],
referrer: getReferrer(account!, paywallConfig, lockAddress),
referrer: referrers[0],
data: purchaseData?.[0],
recurringPayment: recurringPayments
? recurringPayments[0]
Expand All @@ -179,6 +180,11 @@ export function ConfirmCrypto({
onErrorCallback
)
} else {
const referrers = await getReferrers(
recipients,
paywallConfig,
lockAddress
)
await walletService.purchaseKeys(
{
lockAddress,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ export function Select({ checkoutService }: Props) {
props.network || paywallConfig.network || 1

const lockData = await web3Service.getLock(lock, networkId)

let price

if (account) {
Expand Down
5 changes: 3 additions & 2 deletions unlock-app/src/components/interface/keychain/Extend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { config } from '~/config/app'
import { useWeb3Service } from '~/utils/withWeb3Service'
import { Placeholder } from '@unlock-protocol/ui'
import { Key } from '~/hooks/useKeys'
import { getReferrer } from '~/utils/checkoutLockUtils'
import { getReferrers } from '~/utils/checkoutLockUtils'

const ExtendMembershipPlaceholder = () => {
return (
Expand Down Expand Up @@ -121,10 +121,11 @@ export const ExtendMembershipModal = ({
walletService.signer
)
} else {
const referrers = await getReferrers([account!])
await walletService.extendKey({
lockAddress: lock?.address,
tokenId: ownedKey.tokenId,
referrer: getReferrer(account!),
referrer: referrers[0],
recurringPayment: renewal ? renewal : undefined,
totalApproval: unlimited ? MAX_UINT : undefined,
})
Expand Down
11 changes: 7 additions & 4 deletions unlock-app/src/hooks/useCrossChainRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useWeb3Service } from '~/utils/withWeb3Service'
import { Lock } from '~/unlockTypes'
import { useAuth } from '~/contexts/AuthenticationContext'
import { purchasePriceFor } from './usePricing'
import { getReferrer } from '~/utils/checkoutLockUtils'
import { getReferrers } from '~/utils/checkoutLockUtils'
import { CrossChainRoute, getCrossChainRoute } from '~/utils/theBox'
import { networks } from '@unlock-protocol/networks'
import { BoxEvmChains } from '@decent.xyz/box-common'
Expand Down Expand Up @@ -150,15 +150,18 @@ export const useCrossChainRoutes = ({
) {
return null
}
const referrers = await getReferrers(
[account!],
paywallConfig,
lock.address
)
const route = await getCrossChainRoute({
sender: account!,
lock,
prices,
recipients,
keyManagers: keyManagers || recipients,
referrers: recipients.map(() =>
getReferrer(account!, paywallConfig, lock.address)
),
referrers: recipients.map(() => referrers[0]),
purchaseData: purchaseData || recipients.map(() => '0x'),
srcToken: token.address,
srcChainId: network,
Expand Down
17 changes: 17 additions & 0 deletions unlock-app/src/hooks/useGetReferrers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { useQuery } from '@tanstack/react-query'
import { PaywallConfigType } from '@unlock-protocol/core'

import { getReferrers } from '~/utils/checkoutLockUtils'

export const useGetReferrers = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why we need a hook for this and we could not call getReferrers when we need ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(you actually do that in several places and that's correct!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since getReferrers is not sync and it does make rpc calls, I am using hook. react query caches it based on the inputs to avoid making extra calls on re-renders causing issues.

recipients: string[],
paywallConfig: PaywallConfigType,
lockAddress?: string
) => {
return useQuery({
queryKey: ['getReferrers', paywallConfig, lockAddress, recipients],
queryFn: async () => {
return getReferrers(recipients, paywallConfig, lockAddress)
},
})
}
6 changes: 3 additions & 3 deletions unlock-app/src/hooks/usePricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useQuery } from '@tanstack/react-query'
import { PaywallConfigType } from '@unlock-protocol/core'
import { networks } from '@unlock-protocol/networks'
import { ethers } from 'ethers'
import { getReferrer } from '~/utils/checkoutLockUtils'
import { getReferrers } from '~/utils/checkoutLockUtils'
import { useWeb3Service } from '~/utils/withWeb3Service'

interface Options {
Expand Down Expand Up @@ -33,14 +33,14 @@ export const purchasePriceFor = async (
? await web3Service.getTokenDecimals(currencyContractAddress!, network)
: networks[network].nativeCurrency?.decimals || 18

const referrers = await getReferrers(recipients, paywallConfig, lockAddress)
const prices = await Promise.all(
recipients.map(async (userAddress, index) => {
const referrer = getReferrer(userAddress, paywallConfig, lockAddress)
const options = {
lockAddress,
network,
userAddress,
referrer,
referrer: referrers[index],
data: data?.[index] || '0x',
}
const price = await web3Service.purchasePriceFor(options)
Expand Down
65 changes: 50 additions & 15 deletions unlock-app/src/utils/checkoutLockUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
// type so that it at least includes as optional all possible
// properties on a lock. These are all compatible with RawLock insofar

import { PaywallConfigType } from '@unlock-protocol/core'
import { isEns } from '@unlock-protocol/ui'

import { Lock } from '~/unlockTypes'
import { isAccount } from '../utils/checkoutValidators'
import { locksmith } from '~/config/locksmith'
import { getCurrencySymbol } from './currency'
import { PaywallConfigType } from '@unlock-protocol/core'
import { onResolveName } from './resolvers'

// as they only extend it with properties that may be undefined.
interface LockKeysAvailableLock {
Expand Down Expand Up @@ -145,29 +148,61 @@ export const inClaimDisallowList = (address: string) => {
return claimDisallowList.indexOf(address) > -1
}

/**
* Helper function that returns a valid referrer address
* @param recipient
* @param paywallConfig
* @param lockAddress
* @returns
*/
export const getReferrer = (
recipient: string,
export const shouldReferrerResolveForENS = (
paywallConfig?: PaywallConfigType,
lockAddress?: string
): string => {
) => {
if (paywallConfig) {
if (
lockAddress &&
paywallConfig.locks[lockAddress] &&
isAccount(paywallConfig.locks[lockAddress].referrer)
) {
return paywallConfig.locks[lockAddress].referrer!
return false
}
if (paywallConfig.referrer && isAccount(paywallConfig.referrer)) {
return paywallConfig.referrer
if (paywallConfig.referrer && isEns(paywallConfig.referrer)) {
return true
}
}
return recipient
return false
}

export const getReferrers = async (
recipients: string[],
paywallConfig?: PaywallConfigType,
lockAddress?: string
): Promise<string[]> => {
const isReferrerAddressEns = shouldReferrerResolveForENS(
paywallConfig,
lockAddress
)
if (isReferrerAddressEns) {
if (isReferrerAddressEns && paywallConfig && paywallConfig.referrer) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason to change this to be here is that if the referrer is an ENS it will be the same for all. There is no reason to call this for all recipients since that will unnecessarily make quite a few ENS calls.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This codepath will be executed for any existing ENS addresses on config before this change goes live. since it will be formatted to address from here on out.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure the referrer should ever be an ENS. You are resolving the ENS in the settings UI and the config only receives the actual address, which, IMO is good!

Copy link
Contributor Author

@sudheerDev sudheerDev Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally, if the config does use an ENS, instead of having an address, the paywall UI should be able to "resolve" that ENS and use the resolved address when sending transactions.

The issue states to handle this scenario, hence I added it. Let me know if my assumption here is wrong. If we don't have to resolve an ENS from config all we need is small change of using <AddressInput>

try {
// paywallConfig.referrer is always a string if isReferrerAddressEns is true
const response = await onResolveName(paywallConfig.referrer)
if (response && response.address && response.address !== null) {
return recipients.map(() => response.address as string)
}
} catch (e) {
console.log('Error resolving referrer ENS', e)
}
}
}

return recipients.map((recipient) => {
if (paywallConfig) {
if (
lockAddress &&
paywallConfig.locks[lockAddress] &&
isAccount(paywallConfig.locks[lockAddress].referrer)
) {
return paywallConfig.locks[lockAddress].referrer!
}
if (paywallConfig.referrer && isAccount(paywallConfig.referrer)) {
return paywallConfig.referrer
}
}
return recipient
})
}
Loading