Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
5 changes: 0 additions & 5 deletions .github/workflows/deploy-ai-credits-web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ on:
paths:
- 'apps/ai-credits-web/**'
- 'packages/ai-credits-widget/**'
- 'packages/core/**'
- 'packages/ui/**'
- 'packages/embed/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'

concurrency:
group: deploy-ai-credits-web-${{ github.event.pull_request.number || github.ref }}
Expand Down
6 changes: 0 additions & 6 deletions .github/workflows/deploy-superfluid-campaign-web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,6 @@ on:
paths:
- 'apps/superfluid-campaign-web/**'
- 'packages/superfluid-campaign-widget/**'
- 'packages/citizen-claim-widget/**'
- 'packages/core/**'
- 'packages/ui/**'
- 'packages/embed/**'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'

concurrency:
group: deploy-superfluid-campaign-web-${{ github.event.pull_request.number || github.ref }}
Expand Down
69 changes: 69 additions & 0 deletions .github/workflows/playwright-demo-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Playwright Demo Tests

on:
push:
branches:
- main
paths:
- '.github/workflows/playwright-demo-tests.yml'
- 'examples/storybook/**'
- 'packages/**'
- 'playwright.config.ts'
- 'tests/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
pull_request:
branches:
- main
paths:
- '.github/workflows/playwright-demo-tests.yml'
- 'examples/storybook/**'
- 'packages/**'
- 'playwright.config.ts'
- 'tests/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'

permissions:
contents: read

jobs:
playwright-demo:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.15.0

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build packages
run: pnpm run build

- name: Install Playwright browsers
run: pnpm exec playwright install chromium --with-deps

- name: Run Playwright demo tests
run: pnpm test:demo

- name: Upload Playwright artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-demo-artifacts
path: |
playwright-report
test-results
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react'
import React, { useEffect, useMemo, useState } from 'react'
import { GoodWidgetProvider } from '@goodwidget/core'
import { YStack } from '@goodwidget/ui'
import {
Expand Down
23 changes: 21 additions & 2 deletions packages/citizen-claim-widget/src/CitizenClaimWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,13 @@ function CitizenClaimInner({
})

try {
const receipt = await actions.claim()
const receipt = await actions.claim(() =>
updateToast(toastId, {
message: `Claiming on ${singleChainName} — waiting for blockchain confirmation`,
status: 'confirming',
duration: 0,
}),
)
updateToast(toastId, {
message: `Claim succeeded on ${singleChainName}`,
status: 'success',
Expand Down Expand Up @@ -294,7 +300,20 @@ function CitizenClaimInner({
)
}

const claimResults = await actions.claimAll(claimPlan.map((entry) => entry.chainId))
const claimResults = await actions.claimAll(
claimPlan.map((entry) => entry.chainId),
(submittedChainId) => {
const toastId = toastByChain.get(submittedChainId)
if (!toastId) return
const entryChainName =
chainNameById.get(submittedChainId) ?? getChainDisplayName(submittedChainId)
updateToast(toastId, {
message: `Claiming on ${entryChainName} — waiting for blockchain confirmation`,
status: 'confirming',
duration: 0,
})
},
)

for (const claimResult of claimResults) {
const entryChainName =
Expand Down
58 changes: 37 additions & 21 deletions packages/citizen-claim-widget/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,10 @@ export function useCitizenClaimAdapter(
// Transitions: eligible → claiming → success | error
// ---------------------------------------------------------------------------
const claimOnChain = useCallback(
async (targetChainId: number): Promise<unknown> => {
async (
targetChainId: number,
onTransactionSubmitted?: (chainId: number) => void,
): Promise<unknown> => {
if (!isCustodialExecution && !provider) {
throw new CitizenClaimAdapterError('No wallet provider available')
}
Expand Down Expand Up @@ -694,7 +697,14 @@ export function useCitizenClaimAdapter(
)
}

return sdk.claimSDK.claim()
// Pass onTransactionSubmitted as the second argument to claim() so it fires
// immediately after the wallet signs and the tx hash is returned, before the
// receipt is awaited. The citizen-sdk ClaimSDK.submitAndWait already accepts
// an onHash callback; claim() will be updated to thread it through in
// GoodDollar/GoodSDKs (see companion PR). Until that SDK release lands,
// the cast below prevents a compile error.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (sdk.claimSDK as any).claim(undefined, () => onTransactionSubmitted?.(targetChainId))
},
[address, availableChainIds, createSdkInstancesForChain, isCustodialExecution, provider, switchChain],
)
Expand Down Expand Up @@ -726,14 +736,17 @@ export function useCitizenClaimAdapter(
)

const claimAll = useCallback(
async (targetChainIds: number[]): Promise<CitizenClaimWidgetChainClaimResult[]> => {
async (
targetChainIds: number[],
onTransactionSubmitted?: (chainId: number) => void,
): Promise<CitizenClaimWidgetChainClaimResult[]> => {
const chainIdsToClaim = [...new Set(targetChainIds)]

if (isCustodialExecution) {
const settled = await Promise.allSettled(
chainIdsToClaim.map(async (targetChainId) => ({
chainId: targetChainId,
receipt: await claimOnChain(targetChainId),
receipt: await claimOnChain(targetChainId, () => onTransactionSubmitted?.(targetChainId)),
})),
)

Expand All @@ -758,7 +771,7 @@ export function useCitizenClaimAdapter(
results.push({
chainId: targetChainId,
status: 'fulfilled',
receipt: await claimOnChain(targetChainId),
receipt: await claimOnChain(targetChainId, () => onTransactionSubmitted?.(targetChainId)),
})
} catch (claimError: unknown) {
results.push({
Expand All @@ -773,24 +786,27 @@ export function useCitizenClaimAdapter(
[claimOnChain, isCustodialExecution],
)

const handleClaim = useCallback(async (): Promise<unknown> => {
if (!chainId) throw new Error('No active chain selected')
const handleClaim = useCallback(
async (onTransactionSubmitted?: (chainId: number) => void): Promise<unknown> => {
if (!chainId) throw new Error('No active chain selected')

setStatus('claiming')
setError(null)
setStatus('claiming')
setError(null)

try {
const receipt = await claimOnChain(chainId)
if (!mountedRef.current) return receipt
await loadClaimStatus()
return receipt
} catch (err: unknown) {
if (!mountedRef.current) throw err
setStatus('error')
setError(humanReadableError(err))
throw err
}
}, [chainId, claimOnChain, loadClaimStatus])
try {
const receipt = await claimOnChain(chainId, onTransactionSubmitted)
if (!mountedRef.current) return receipt
await loadClaimStatus()
return receipt
} catch (err: unknown) {
if (!mountedRef.current) throw err
setStatus('error')
setError(humanReadableError(err))
throw err
}
},
[chainId, claimOnChain, loadClaimStatus],
)

// ---------------------------------------------------------------------------
// handleVerify — initiates the GoodID face-verification flow.
Expand Down
17 changes: 14 additions & 3 deletions packages/citizen-claim-widget/src/widgetRuntimeContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,20 @@ export interface CitizenClaimWidgetAdapterActions {
connect: () => Promise<void>
refresh: () => Promise<void>
startVerification: () => Promise<void>
claim: () => Promise<unknown>
claimOnChain: (chainId: number) => Promise<unknown>
claimAll: (chainIds: number[]) => Promise<CitizenClaimWidgetChainClaimResult[]>
/**
* `onTransactionSubmitted` fires once the wallet has signed and broadcast
* the transaction, ahead of on-chain confirmation — lets callers move a
* "sign in your wallet" toast to a "waiting for confirmation" state.
*/
claim: (onTransactionSubmitted?: (chainId: number) => void) => Promise<unknown>
claimOnChain: (
chainId: number,
onTransactionSubmitted?: (chainId: number) => void,
) => Promise<unknown>
claimAll: (
chainIds: number[],
onTransactionSubmitted?: (chainId: number) => void,
) => Promise<CitizenClaimWidgetChainClaimResult[]>
switchChain?: (chainId: number) => Promise<void>
}

Expand Down
13 changes: 8 additions & 5 deletions packages/ui/src/components/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Spinner } from '../components-test/Spinner'
// Multiple toasts can be visible at once; each is identified by a unique id.
// ---------------------------------------------------------------------------

export type ToastStatus = 'pending' | 'success' | 'error' | 'info'
export type ToastStatus = 'pending' | 'confirming' | 'success' | 'error' | 'info'

export interface ToastConfig {
message: string
Expand Down Expand Up @@ -83,10 +83,11 @@ export function useToast(): ToastItem[] {
* Named 'Toast' so Tamagui resolves light_Toast / dark_Toast component themes.
*
* Status variant adjusts the border accent color to communicate the toast type:
* pending → primary (blue)
* success → success (green)
* error → error (red)
* info → primary (blue)
* pending → primary (blue) — waiting on the wallet to sign
* confirming → primaryDark (deeper blue) — signed and broadcast, waiting on-chain
* success → success (green)
* error → error (red)
* info → primary (blue)
*/
const ToastFrame = createComponent(Stack, {
name: 'Toast',
Expand All @@ -108,6 +109,7 @@ const ToastFrame = createComponent(Stack, {
variants: {
status: {
pending: { borderColor: '$primary' },
confirming: { borderColor: '$primaryDark' },
success: { borderColor: '$success' },
error: { borderColor: '$error' },
info: { borderColor: '$primary' },
Expand Down Expand Up @@ -161,6 +163,7 @@ function StatusIcon({ status }: { status?: ToastStatus }) {
if (!status) return null
switch (status) {
case 'pending':
case 'confirming':
return <Spinner size="sm" />
case 'success':
return <Icon name="check" size="xs" color="success" />
Expand Down
Loading