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
20 changes: 14 additions & 6 deletions packages/citizen-claim-widget/src/InviteRewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,15 @@ import { decodeInviteCode, formatInviteBounty, useInviteRuntime } from './invite
import { canAttachInviter, hasCollectableInvitees, isInviteeCollectable } from './inviteRules'

/**
* "How it works" — mirrors GoodWallet's InviteView, which opens the explainer
* in a Drawer from an inline info-icon link rather than showing it inline.
* "How it works" — mirrors GoodWallet's InviteView steps. Copy is hardcoded here
* (English-only, kept simple for now); minimumDays/minimumClaims are the only
* parts that must stay accurate, so those come from InvitesV2 via
* `state.minimums` (InviteSDK.getMinimums()) rather than being hardcoded too.
*/
function HowItWorksDrawer() {
const { state } = useInviteRuntime()
const [open, setOpen] = useState(false)
const { minimums } = state

return (
<>
Expand All @@ -37,12 +41,16 @@ function HowItWorksDrawer() {
<Drawer open={open} onClose={() => setOpen(false)}>
<YStack gap="$4">
<Heading level={3}>How it works</Heading>
<Text secondary>1. Share your code.</Text>
<Text secondary>2. Your friend joins and claims.</Text>
<Text secondary>1. Share your personal invite link with your friends.</Text>
<Text secondary>
2. Make sure they sign up and complete{' '}
{minimums ? minimums.minimumClaims : 'the required number of'} claims.
</Text>
<Text secondary>
3. After identity, claim-day, and minimum-claim requirements are met, collect your
reward.
3. After {minimums ? minimums.minimumDays : 'the required number of'} days, the
invite reward becomes available for them to claim.
</Text>
<Text secondary>4. When they claim it, both of you receive the reward.</Text>
<Button fullWidth onPress={() => setOpen(false)}>
<ButtonText>Close</ButtonText>
</Button>
Expand Down
27 changes: 20 additions & 7 deletions packages/citizen-claim-widget/src/inviteAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export type InviteStatus =
| 'collecting'
| 'error'

export interface InviteMinimums {
minimumDays: number
minimumClaims: number
}

export interface InviteState {
status: InviteStatus
address: Address | null
Expand All @@ -44,6 +49,8 @@ export interface InviteState {
collectableInvitees: Address[]
eligibility: Record<string, BountyEligibilityDetails>
selfEligibility: BountyEligibilityDetails | null
/** Global minimumDays/minimumClaims thresholds, read from InvitesV2 via InviteSDK.getMinimums(). */
minimums: InviteMinimums | null
error: string | null
success: string | null
}
Expand Down Expand Up @@ -79,6 +86,7 @@ const initialInviteState: InviteState = {
collectableInvitees: [],
eligibility: {},
selfEligibility: null,
minimums: null,
error: null,
success: null,
}
Expand Down Expand Up @@ -130,6 +138,7 @@ export interface InviteSnapshotSdk {
getPendingInvitees: InviteSDK['getPendingInvitees']
getCollectableInvitees: InviteSDK['getCollectableInvitees']
checkEligibilityDetails: InviteSDK['checkEligibilityDetails']
getMinimums: InviteSDK['getMinimums']
}

export interface InviteSnapshot {
Expand All @@ -140,6 +149,7 @@ export interface InviteSnapshot {
collectableInvitees: Address[]
eligibility: Record<string, BountyEligibilityDetails>
selfEligibility: BountyEligibilityDetails | null
minimums: InviteMinimums
}

/**
Expand All @@ -152,13 +162,15 @@ export async function loadInviteSnapshot(
address: Address,
): Promise<InviteSnapshot> {
const user = await sdk.getUser(address)
const [level, invitees, pendingInvitees, collectableInvitees, selfEligibility] = await Promise.all([
sdk.getLevel(Number(user.level)),
sdk.getInvitees(address),
sdk.getPendingInvitees(address),
sdk.getCollectableInvitees(address),
sdk.checkEligibilityDetails(address),
])
const [level, invitees, pendingInvitees, collectableInvitees, selfEligibility, minimums] =
await Promise.all([
sdk.getLevel(Number(user.level)),
sdk.getInvitees(address),
sdk.getPendingInvitees(address),
sdk.getCollectableInvitees(address),
sdk.checkEligibilityDetails(address),
sdk.getMinimums(),
])
const eligibilityEntries = await Promise.all(
pendingInvitees.map(async (invitee) => {
const { details } = await sdk.checkEligibilityDetails(invitee)
Expand All @@ -174,6 +186,7 @@ export async function loadInviteSnapshot(
collectableInvitees,
eligibility: Object.fromEntries(eligibilityEntries),
selfEligibility: selfEligibility.details,
minimums,
}
}

Expand Down
5 changes: 5 additions & 0 deletions tests/widgets/citizen-claim-widget/inviteAdapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ function createFakeSdk(overrides: Partial<InviteWriteSdk> = {}): InviteWriteSdk
getPendingInvitees: async () => [invitee, collectableInvitee],
getCollectableInvitees: async () => [collectableInvitee],
checkEligibilityDetails: async () => ({ eligible: true, details: eligibility }),
getMinimums: async () => ({
minimumClaims: eligibility.minimumClaims,
minimumDays: eligibility.minimumDays,
}),
resolveCode: async () => zeroAddress,
join: async () => '0xjoin' as `0x${string}`,
collectAllBounties: async () => [],
Expand All @@ -64,6 +68,7 @@ test('loadInviteSnapshot distinguishes pending from collectable invitees', async
expect(snapshot.collectableInvitees).toEqual([collectableInvitee])
expect(isInviteeCollectable(invitee, snapshot.collectableInvitees)).toBe(false)
expect(isInviteeCollectable(collectableInvitee, snapshot.collectableInvitees)).toBe(true)
expect(snapshot.minimums).toEqual({ minimumClaims: 5, minimumDays: 3 })
})

test('performJoin reuses the caller original code when attaching a deferred inviter', async () => {
Expand Down