Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
19 changes: 17 additions & 2 deletions e2e/faucet.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect, test } from '@playwright/test'
import { getDemoStep } from './helpers'

test('fund an address via faucet', async ({ page }) => {
test.setTimeout(120000)
Expand Down Expand Up @@ -26,8 +27,8 @@ test('fund an address via faucet', async ({ page }) => {
)
})

test('fund a connected passkey wallet via faucet', async ({ page }) => {
test.setTimeout(240000)
test('fund a connected passkey wallet and send a first payment', async ({ page }) => {
test.setTimeout(300000)

const client = await page.context().newCDPSession(page)
await client.send('WebAuthn.enable')
Expand Down Expand Up @@ -73,6 +74,20 @@ test('fund a connected passkey wallet via faucet', async ({ page }) => {
await expect(addFundsStep.getByRole('button', { name: 'Add more funds' })).toBeVisible({
timeout: 120000,
})

const handoff = page.getByRole('link', { name: 'Send your first testnet payment' })
await expect(handoff).toBeVisible()
await handoff.click()
await expect(page).toHaveURL(/\/docs\/guide\/payments\/send-a-payment#send-payment-demo$/)

const sendPaymentStep = getDemoStep(page, 'Send 100 AlphaUSD to a recipient.')
const enterDetailsButton = sendPaymentStep.getByRole('button', { name: 'Enter details' })
await expect(enterDetailsButton).toBeEnabled({ timeout: 90000 })
await enterDetailsButton.click()
await sendPaymentStep.getByRole('button', { name: 'Send' }).click()
await expect(sendPaymentStep.getByRole('link', { name: 'View receipt' })).toBeVisible({
timeout: 90000,
})
} finally {
await client.send('WebAuthn.removeVirtualAuthenticator', { authenticatorId }).catch(() => {})
}
Expand Down
180 changes: 180 additions & 0 deletions src/components/guides/FaucetActivationExperiment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
'use client'

import * as React from 'react'
import {
bucketClaimDuration,
captureDeveloperActivationEvent,
categorizeActivationFailure,
FAUCET_ACTIVATION_EXPERIMENT_KEY,
type FaucetActivationVariant,
type FaucetClaimMethod,
readFaucetActivationJourney,
resolveFaucetActivationAssignment,
startFaucetActivationJourney,
trackDeveloperActivationQuickstart,
updateFaucetActivationJourney,
} from '../../lib/developer-activation'
import { onPostHogReady } from '../../lib/posthog'

type FaucetActivationContextValue = {
claimFailed: (method: FaucetClaimMethod, error: unknown) => void
claimStarted: (method: FaucetClaimMethod) => void
claimSucceeded: (method: FaucetClaimMethod, receiptCount: number) => void
hasSuccessfulClaim: boolean
variant: FaucetActivationVariant
}

const emptyContext: FaucetActivationContextValue = {
claimFailed: () => {},
claimStarted: () => {},
claimSucceeded: () => {},
hasSuccessfulClaim: false,
variant: 'unassigned',
}

const FaucetActivationContext = React.createContext<FaucetActivationContextValue>(emptyContext)

export function useFaucetActivation() {
return React.useContext(FaucetActivationContext)
}

export function FaucetActivationProvider({ children }: React.PropsWithChildren) {
const e2eVariant = import.meta.env.VITE_E2E === 'true' ? ('guided_handoff' as const) : null
const [variant, setVariant] = React.useState<FaucetActivationVariant>(e2eVariant ?? 'unassigned')
const [hasSuccessfulClaim, setHasSuccessfulClaim] = React.useState(false)
const variantRef = React.useRef<FaucetActivationVariant>(e2eVariant ?? 'unassigned')
const variantLockedRef = React.useRef(Boolean(e2eVariant))
const viewedRef = React.useRef(false)
const claimStartedAtRef = React.useRef<Partial<Record<FaucetClaimMethod, number>>>({})

const trackView = React.useCallback((assignedVariant: FaucetActivationVariant) => {
if (viewedRef.current) return
viewedRef.current = true
captureDeveloperActivationEvent('faucet_viewed', assignedVariant, {})
}, [])

React.useEffect(() => {
const existingJourney = readFaucetActivationJourney()
if (existingJourney) {
variantRef.current = existingJourney.experimentVariant
variantLockedRef.current = true
setVariant(existingJourney.experimentVariant)
setHasSuccessfulClaim(true)
trackView(existingJourney.experimentVariant)
return
}

if (e2eVariant) {
trackView(e2eVariant)
return
}

let unsubscribeFeatureFlags: (() => void) | undefined
const unsubscribeReady = onPostHogReady((posthog) => {
unsubscribeFeatureFlags?.()
unsubscribeFeatureFlags = posthog.onFeatureFlags((_flags, _variants, context) => {
const assignedVariant = resolveFaucetActivationAssignment({
errorsLoading: context?.errorsLoading,
featureFlag: posthog.getFeatureFlag(FAUCET_ACTIVATION_EXPERIMENT_KEY),
locked: variantLockedRef.current,
})
if (!assignedVariant) return
variantRef.current = assignedVariant
variantLockedRef.current = true
setVariant(assignedVariant)
trackView(assignedVariant)
})
})

return () => {
unsubscribeFeatureFlags?.()
unsubscribeReady()
}
}, [e2eVariant, trackView])

const claimStarted = React.useCallback(
(method: FaucetClaimMethod) => {
const assignedVariant = variantRef.current
variantLockedRef.current = true
claimStartedAtRef.current[method] = Date.now()
trackView(assignedVariant)
captureDeveloperActivationEvent('faucet_claim_started', assignedVariant, {
claim_method: method,
})
},
[trackView],
)

const claimSucceeded = React.useCallback((method: FaucetClaimMethod, receiptCount: number) => {
const now = Date.now()
const assignedVariant = variantRef.current
const startedAt = claimStartedAtRef.current[method]
const { persisted } = startFaucetActivationJourney(method, assignedVariant, now)
captureDeveloperActivationEvent('faucet_claim_succeeded', assignedVariant, {
claim_duration_bucket: bucketClaimDuration(
startedAt === undefined ? undefined : now - startedAt,
),
claim_method: method,
journey_persisted: persisted,
receipt_count: receiptCount,
})
setHasSuccessfulClaim(true)
}, [])

const claimFailed = React.useCallback((method: FaucetClaimMethod, error: unknown) => {
const now = Date.now()
const startedAt = claimStartedAtRef.current[method]
captureDeveloperActivationEvent('faucet_claim_failed', variantRef.current, {
claim_duration_bucket: bucketClaimDuration(
startedAt === undefined ? undefined : now - startedAt,
),
claim_method: method,
failure_category: categorizeActivationFailure(error),
})
}, [])

const value = React.useMemo(
() => ({ claimFailed, claimStarted, claimSucceeded, hasSuccessfulClaim, variant }),
[claimFailed, claimStarted, claimSucceeded, hasSuccessfulClaim, variant],
)

return (
<FaucetActivationContext.Provider value={value}>{children}</FaucetActivationContext.Provider>
)
}

export function FaucetActivationHandoff() {
const { hasSuccessfulClaim, variant } = useFaucetActivation()
if (!hasSuccessfulClaim || variant !== 'guided_handoff') return null

const destination = '/docs/guide/payments/send-a-payment#send-payment-demo'
return (
<aside className="mt-6 rounded-2xl border border-gray4 bg-gray1 p-6" aria-live="polite">
<h2 className="font-medium text-[18px] text-gray12">Send your first testnet payment</h2>
<ol className="mt-3 list-decimal space-y-1 ps-5 text-[14px] text-gray10">
<li>Open the payment quickstart.</li>
<li>Send an AlphaUSD testnet payment.</li>
<li>Confirm delivery from the transaction receipt.</li>
</ol>
<div className="mt-5 flex flex-wrap items-center gap-4">
<a
className="rounded-full bg-accent px-4 py-2 font-medium text-[14px] text-white"
href={destination}
onClick={() => updateFaucetActivationJourney({ handoffClicked: true })}
>
Send your first testnet payment
</a>
<a className="text-[14px] text-accent hover:underline" href="/docs">
Browse the docs
</a>
</div>
</aside>
)
}

export function DeveloperActivationQuickstartTracker() {
React.useEffect(() => {
trackDeveloperActivationQuickstart()
}, [])
return null
}
24 changes: 19 additions & 5 deletions src/components/guides/steps/payments/AddFundsToOthers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ import { mnemonicToAccount } from 'viem/accounts'
import { Actions } from 'viem/tempo'
import { useBlockNumber, useClient, useConnection } from 'wagmi'
import { Hooks } from 'wagmi/tempo'
import { assertSuccessfulFaucetReceipts } from '../../../../lib/developer-activation'
import { Button, ExplorerAccountLink, Step } from '../../Demo'
import { useFaucetActivation } from '../../FaucetActivationExperiment'
import { alphaUsd } from '../../tokens'
import type { DemoStepProps } from '../types'

export function AddFundsToOthers(props: DemoStepProps) {
const { stepNumber = 2, last = false } = props
const { address } = useConnection()
const queryClient = useQueryClient()
const activation = useFaucetActivation()
const [fundAddress, setFundAddress] = React.useState<string | undefined>(undefined)

// Initialize fundAddress with connected wallet address (only once)
Expand All @@ -37,7 +40,6 @@ export function AddFundsToOthers(props: DemoStepProps) {
refetchInterval: 1_500,
},
})
// biome-ignore lint/correctness/useExhaustiveDependencies: _
React.useEffect(() => {
balanceRefetch()
}, [blockNumber])
Expand All @@ -47,7 +49,7 @@ export function AddFundsToOthers(props: DemoStepProps) {
if (!isValidTarget) throw new Error('valid target address not found')
if (!client) throw new Error('client not found')

let receipts = null
let receipts: readonly { status?: string }[]
if (import.meta.env.VITE_TEMPO_ENV !== 'localnet')
receipts = await Actions.faucet.fundSync(client as unknown as Client<Transport, Chain>, {
account: targetAddress as Address,
Expand All @@ -66,12 +68,24 @@ export function AddFundsToOthers(props: DemoStepProps) {
)
receipts = [result.receipt]
}
assertSuccessfulFaucetReceipts(receipts)
await new Promise((resolve) => setTimeout(resolve, 400))
queryClient.refetchQueries({ queryKey: ['getBalance'] })
return receipts
},
onSuccess(receipts) {
activation.claimSucceeded('address_form', receipts.length)
},
onError(error) {
activation.claimFailed('address_form', error)
},
})

const claimFunds = React.useCallback(() => {
activation.claimStarted('address_form')
fundAccount.mutate()
}, [activation, fundAccount.mutate])

const active = React.useMemo(() => {
// If this is the last step, simply has to be logged in
if (last) return true
Expand All @@ -87,7 +101,7 @@ export function AddFundsToOthers(props: DemoStepProps) {
disabled={!isValidTarget || fundAccount.isPending}
variant="default"
className="font-normal text-[14px] -tracking-[2%]"
onClick={() => fundAccount.mutate()}
onClick={claimFunds}
type="button"
>
{fundAccount.isPending ? 'Adding funds' : 'Add more funds'}
Expand All @@ -99,7 +113,7 @@ export function AddFundsToOthers(props: DemoStepProps) {
variant={isValidTarget ? 'accent' : 'default'}
className="font-normal text-[14px] -tracking-[2%]"
type="button"
onClick={() => fundAccount.mutate()}
onClick={claimFunds}
>
{fundAccount.isPending ? 'Adding funds' : 'Add funds'}
</Button>
Expand All @@ -109,7 +123,7 @@ export function AddFundsToOthers(props: DemoStepProps) {
balance,
fundAccount.isPending,
fundAccount.isSuccess,
fundAccount.mutate,
claimFunds,
fundAccount,
])

Expand Down
16 changes: 16 additions & 0 deletions src/components/guides/steps/payments/SendPayment.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import * as React from 'react'
import { isAddress, pad, parseUnits, stringToHex } from 'viem'
import { useConnection, useConnectionEffect } from 'wagmi'
import { Hooks } from 'wagmi/tempo'
import {
categorizeActivationFailure,
isDeliveredTestnetPayment,
trackDeveloperActivationPaymentFailed,
trackDeveloperActivationPaymentSucceeded,
} from '../../../../lib/developer-activation'
import { Button, ExplorerLink, FAKE_RECIPIENT, Step } from '../../Demo'
import { alphaUsd } from '../../tokens'
import type { DemoStepProps } from '../types'
Expand All @@ -20,6 +26,16 @@ export function SendPayment(props: DemoStepProps) {
})
const sendPayment = Hooks.token.useTransferSync({
mutation: {
onSuccess(result, variables) {
if (isDeliveredTestnetPayment(result, variables.to)) {
trackDeveloperActivationPaymentSucceeded()
return
}
trackDeveloperActivationPaymentFailed('receive_policy_redirect')
},
onError(error) {
trackDeveloperActivationPaymentFailed(categorizeActivationFailure(error))
},
onSettled() {
balanceRefetch()
},
Expand Down
Loading
Loading