Skip to content

Commit

Permalink
feat(unlock-app): allowing promocode to be passed (#13489)
Browse files Browse the repository at this point in the history
* allowing promocode to be passed as part of the checkout config or in the URL

* adding docs about the use of the promo option
  • Loading branch information
julien51 authored Mar 19, 2024
1 parent 588de46 commit 5fdc5e4
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 29 deletions.
1 change: 1 addition & 0 deletions docs/docs/tools/checkout/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ The `paywallConfig` is a JSON object which includes a set of customizations for
- `expectedAddress`: _optional string_. If set, the user will be asked to switch their wallet address before proceeding. This is useful if you want to ensure that the user is using the same address as the one they used to purchase a membership.

- `skipSelect`: _optional boolean_. Skip selection screen if only single lock is available.
- `promo`: _optional string_. If set, it is used to pre-fill the promo code field on the checkout. a `promo` query string can also be passed as a query param when using the checkout URL. (note: requires the use of the discount code hook!)

### Locks

Expand Down
28 changes: 20 additions & 8 deletions packages/core/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,16 @@ export const PaywallLockConfig = z.object({
})
.optional(),
promo: z
.boolean({
description:
'If enabled, the user will be prompted to enter an optional promo code in order to receive discounts. Warning: This only works if the lock is connected to a hook that will handle the promo codes. This cannot be used at the same time as the "Password Required" option above',
})
.union([
z.boolean({
description:
'If enabled, the user will be prompted to enter an optional promo code in order to receive discounts. Warning: This only works if the lock is connected to a hook that will handle the promo codes. This cannot be used at the same time as the "Password Required" option above',
}),
z.string({
description:
'If set, the user optional promo code field will be pre-filled with this value. Warning: This only works if the lock is connected to a hook that will handle the promo codes. This cannot be used at the same time as the "Password Required" option above',
}),
])
.optional(),
emailRequired: z
.boolean({
Expand Down Expand Up @@ -236,10 +242,16 @@ export const PaywallConfig = z
})
.optional(),
promo: z
.boolean({
description:
'If enabled, the user will be prompted to enter an optional promo code in order to receive discounts. Warning: This only works if the lock is connected to a hook that will handle the promo codes. This cannot be used at the same time as the "Password Required" option above',
})
.union([
z.boolean({
description:
'If enabled, the user will be prompted to enter an optional promo code in order to receive discounts. Warning: This only works if the lock is connected to a hook that will handle the promo codes. This cannot be used at the same time as the "Password Required" option above',
}),
z.string({
description:
'If set, the user optional promo code field will be pre-filled with this value. Warning: This only works if the lock is connected to a hook that will handle the promo codes. This cannot be used at the same time as the "Password Required" option above',
}),
])
.optional(),
emailRequired: z
.boolean({
Expand Down
99 changes: 78 additions & 21 deletions unlock-app/src/components/interface/checkout/main/Promo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ import { getEthersWalletFromPassword } from '~/utils/strings'
import { Web3Service } from '@unlock-protocol/unlock-js'
import { useDebounce } from 'react-use'
import LoadingIcon from '../../Loading'
import { useRouter } from 'next/router'
interface Props {
injectedProvider: unknown
checkoutService: CheckoutService
recipients: string[]
lock: any
promoCode?: string
send: (obj: any) => void
}

interface FormData {
Expand All @@ -25,13 +30,33 @@ interface FormData {

const web3Service = new Web3Service(networks)

export function Promo({ injectedProvider, checkoutService }: Props) {
export const computePromoData = async (promo: string, recipients: string[]) => {
const privateKeyAccount = await getEthersWalletFromPassword(promo)
return Promise.all(
recipients.map((address) => {
const messageHash = ethers.utils.solidityKeccak256(
['string'],
[address.toLowerCase()]
)
const messageHashBinary = ethers.utils.arrayify(messageHash)
return privateKeyAccount.signMessage(messageHashBinary)
})
)
}

export function PromoContent({
recipients,
lock,
promoCode,
send,
injectedProvider,
checkoutService,
}: Props) {
const { account } = useAuth()
const [state, send] = useActor(checkoutService)
const [hookAddress, setHookAddress] = useState<string>()
const [code, setCode] = useState<string>()
const [code, setCode] = useState<string | undefined>(promoCode)
const [promoCodeLoading, setPromoCodeLoading] = useState<boolean>(false)
const [promoCodeDetails, setPromoCodeDetails] = useState<any>()
const { recipients, lock } = state.context
const {
register,
handleSubmit,
Expand All @@ -54,6 +79,7 @@ export function Promo({ injectedProvider, checkoutService }: Props) {
useDebounce(
async () => {
if (hookAddress && code) {
setPromoCodeLoading(true)
const privateKeyFromAccount = await getEthersWalletFromPassword(code)
const promoCodeDetails = await web3Service.getDiscountHookWithCapValues(
{
Expand All @@ -64,26 +90,17 @@ export function Promo({ injectedProvider, checkoutService }: Props) {
}
)
setPromoCodeDetails(promoCodeDetails)
setPromoCodeLoading(false)
}
},
300,
[code]
100,
[code, hookAddress, lock]
)

const onSubmit = async (formData: FormData) => {
try {
const { promo } = formData
const privateKeyAccount = await getEthersWalletFromPassword(promo)
const data = await Promise.all(
users.map((address) => {
const messageHash = ethers.utils.solidityKeccak256(
['string'],
[address.toLowerCase()]
)
const messageHashBinary = ethers.utils.arrayify(messageHash)
return privateKeyAccount.signMessage(messageHashBinary)
})
)
const data = await computePromoData(promo, users)
send({
type: 'SUBMIT_DATA',
data,
Expand Down Expand Up @@ -119,6 +136,11 @@ export function Promo({ injectedProvider, checkoutService }: Props) {
return null
}

let error = errors.promo?.message
if (!error && code && promoCodeDetails?.discount === 0) {
error = "We couldn't find a discount for this code. "
}

return (
<Fragment>
<Stepper service={checkoutService} />
Expand All @@ -128,11 +150,17 @@ export function Promo({ injectedProvider, checkoutService }: Props) {
// @ts-ignore
iconRight={iconRight}
label="Enter Promo Code"
description="If you have a promo code to receive discounts, please enter it now."
description={
!promoCodeDetails?.discount
? 'If you have a promo code to receive discounts, please enter it now.'
: ''
}
type="text"
size="small"
{...register('promo')}
error={errors.promo?.message}
{...register('promo', {
value: code,
})}
error={error}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setCode(event.target.value)
setPromoCodeDetails(undefined)
Expand All @@ -149,7 +177,7 @@ export function Promo({ injectedProvider, checkoutService }: Props) {
type="submit"
form="promo"
className="w-full"
disabled={isSubmitting}
disabled={isSubmitting || promoCodeLoading}
loading={isSubmitting}
onClick={handleSubmit(onSubmit)}
>
Expand All @@ -161,3 +189,32 @@ export function Promo({ injectedProvider, checkoutService }: Props) {
</Fragment>
)
}

interface PromoProps {
injectedProvider: unknown
checkoutService: CheckoutService
}

export function Promo({ injectedProvider, checkoutService }: PromoProps) {
const [state, send] = useActor(checkoutService)
const { query } = useRouter()
const { recipients, lock, paywallConfig } = state.context

let promoCode = ''
if (query?.promo) {
promoCode = query?.promo.toString()
} else if (typeof paywallConfig.locks[lock!.address].promo === 'string') {
promoCode = paywallConfig.locks[lock!.address].promo as string
}

return (
<PromoContent
recipients={recipients}
lock={lock}
promoCode={promoCode}
send={send}
injectedProvider={injectedProvider}
checkoutService={checkoutService}
/>
)
}

0 comments on commit 5fdc5e4

Please sign in to comment.