Fix multi-chain claim flow under AppKit connections (standalone Superfluid campaign widget) - #165
Conversation
…in, multi-chain claims, network-settings label (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
… copy and live active chain (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…upported chain, not just the active one (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…h-chain overrides (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…tValue and switchChain (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…ops to GoodWidgetProviderProps (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…ain-id lookups (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…T_APPKIT_NETWORKS (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…nto runtime and embedded claim widget (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
… through CampaignHeader (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
… through LeaderboardView (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…le disconnect label in WalletChip (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…/switch-chain/disconnect-label overrides (#164) On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
There was a problem hiding this comment.
Pull request overview
Note
Copilot could not run the full agentic suite for this review because it was automatically requested on a bot-authored pull request. Request a review from Copilot under Reviewers to retry with the full agentic suite. Improved support for bot-authored pull requests is coming soon.
Adds integrator-controlled wallet state and UX overrides across widgets, improving AppKit compatibility and multi-chain claim behavior.
Changes:
- Extend widget/provider props to accept address/chain overrides, a chain-switch override, and a configurable wallet-chip action label.
- Centralize chain switching in
GoodWidgetProviderand update Citizen Claim adapter to use it + improve per-chain entitlement reads. - Update Superfluid campaign UI to forward disconnect label and embedded claim widget wallet overrides; expose AppKit default networks for embedding.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/superfluid-campaign-widget/src/widgetRuntimeContract.ts | Adds new integrator override props to Superfluid Campaign widget contract. |
| packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx | Makes wallet chip action label configurable. |
| packages/superfluid-campaign-widget/src/components/LeaderboardView.tsx | Threads disconnectLabel through to WalletChip. |
| packages/superfluid-campaign-widget/src/components/CampaignHeader.tsx | Threads disconnectLabel through to WalletChip. |
| packages/superfluid-campaign-widget/src/SuperfluidCampaignWidget.tsx | Forwards wallet overrides into embedded Citizen Claim widget and surfaces disconnect label. |
| packages/embed/src/appkitProvider.ts | Re-exports useAppKitNetwork and DEFAULT_APPKIT_NETWORKS for embed consumers. |
| packages/embed/src/DefaultAppKitProvider.tsx | Exports DEFAULT_APPKIT_NETWORKS. |
| packages/core/src/types.ts | Extends GoodWidgetProviderProps with wallet overrides and disconnect label. |
| packages/core/src/provider.tsx | Implements address/chain override logic + adds switchChain and disconnectLabel to wallet context. |
| packages/citizen-claim-widget/src/widgetRuntimeContract.ts | Adds wallet override props to Citizen Claim widget contract. |
| packages/citizen-claim-widget/src/adapter.ts | Uses provider switchChain and improves multi-chain entitlement reads. |
| packages/citizen-claim-widget/src/CitizenClaimWidget.tsx | Shelves UI inside provider to read live chainId and forwards wallet overrides. |
| apps/superfluid-campaign-web/src/App.tsx | Demonstrates AppKit-driven overrides (address/chain/switch + disconnect label). |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
L03TJ3
left a comment
There was a problem hiding this comment.
gdpatchagent will follow up on review
…et): read-only balance reads, chain-scoped execute gating, PR #165 cleanup - Balance/entitlement reads (loadClaimablesByChain, loadClaimStatus) now use address-only, read-only clients built locally, instead of requiring a connected account or the passed-down provider. - On-chain claim execution is now gated to the passed-down provider's available/active chains via a new availableChainIdsOverride prop threaded through GoodWidgetProvider, CitizenClaimWidget and SuperfluidCampaignWidget, and populated from AppKit's approvedCaipNetworkIds in the demo app. - Errors now name the specific chain an action cannot run on (CitizenClaimAdapterError), surfaced verbatim by humanReadableError. - Cleanup: removed the unsafe chainId cast in loadClaimStatus (replaced by isSupportedChain narrowing), fixed the getPublicClientForChain(...) ?? undefined bug, deduped the switch-chain error string, corrected a stale comment in provider.tsx, and replaced WalletChip's hardcoded log-out icon with a disconnectIcon override mirroring disconnectLabel. Addresses PR #165 review feedback. On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
…fix chain-resolving flicker and stale disabled menu loadClaimStatus was mapping a non-custodial connection whose active chain hasn't reported yet (chainId still null right after connect) into unsupported_chain instead of a transient loading state, causing a brief "switch network" flash before the real chain resolves. WalletChip kept its menu open and its disconnect action pressable if disableWalletButton flipped true while the menu was already open. Co-Authored-By: Claude <noreply@anthropic.com> On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
|
Fixed in 00d029c, addressing 2 of Copilot round-7's suppressed low-confidence comments that I judged to be real, in-scope bugs:
The other 2 suppressed comments from round 7 I'm explicitly leaving as-is (nice-to-have, not affecting end-user UX or correctness):
Re-requesting Copilot review now. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (6)
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx:39
- When
disabledflips fromfalse→truewhile the menu is open, the menu stops rendering butisMenuOpenstaystrue. If the chip later becomes enabled again, the menu will immediately reappear without a user click. Consider closing the menu state wheneverdisabledbecomes true (e.g. setisMenuOpento false in an effect or during the disabled transition).
const [isMenuOpen, setIsMenuOpen] = useState(false)
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx:43
- When
disabledflips fromfalse→truewhile the menu is open, the menu stops rendering butisMenuOpenstaystrue. If the chip later becomes enabled again, the menu will immediately reappear without a user click. Consider closing the menu state wheneverdisabledbecomes true (e.g. setisMenuOpento false in an effect or during the disabled transition).
<XStack position="relative" alignItems="center" opacity={disabled ? 0.5 : 1}>
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx:57
- When
disabledflips fromfalse→truewhile the menu is open, the menu stops rendering butisMenuOpenstaystrue. If the chip later becomes enabled again, the menu will immediately reappear without a user click. Consider closing the menu state wheneverdisabledbecomes true (e.g. setisMenuOpento false in an effect or during the disabled transition).
cursor={disabled ? 'not-allowed' : 'pointer'}
onPress={() => {
if (disabled) return
setDisconnectMessage(null)
setIsMenuOpen((open) => !open)
}}
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx:70
- When
disabledflips fromfalse→truewhile the menu is open, the menu stops rendering butisMenuOpenstaystrue. If the chip later becomes enabled again, the menu will immediately reappear without a user click. Consider closing the menu state wheneverdisabledbecomes true (e.g. setisMenuOpento false in an effect or during the disabled transition).
{isMenuOpen && !disabled && (
packages/core/src/provider.tsx:60
- Some connectors/providers surface EIP-1193 user rejection as
code: '4001'(string) rather than4001(number). In that case,isUserRejectedSwitchChainwould return false andswitchChainmay incorrectly fall back toswitchChainOverride, potentially re-prompting the user right after rejection. Consider treating'4001'as a rejection as well.
function isUserRejectedSwitchChain(err: unknown): boolean {
const code = (err as { code?: number | string } | undefined)?.code
return code === 4001 || code === 'ACTION_REJECTED'
}
packages/citizen-claim-widget/src/adapter.ts:398
SUPPORTED_CHAINSis sourced fromcitizenSdkCapabilities.chains, butcreateReadOnlyClientsForChainhard-depends onCHAIN_CONFIGS[targetChainId]having an RPC URL. If capabilities ever include a supported chain that isn’t present inCHAIN_CONFIGS(or lacks an RPC URL), claimable reads for that chain will be silently skipped (and status resolution can degrade tonot_connected/missing data). Consider enforcing thatCHAIN_CONFIGSfully coversSUPPORTED_CHAINS(e.g. assertion at init), or falling back to an existing public-client resolver for that chain.
const chain = CHAIN_CONFIGS[targetChainId]
const rpcUrl = chain?.rpcUrls.default.http[0]
if (!chain || !rpcUrl) return null
…,core): fix stale chainIdOverride, stale wallet-menu reopen, string switch-chain rejection code chainIdOverride was not gated on isAccountResolved like addressOverride, so a disconnected account could keep showing a stale previously-tracked chain instead of falling back correctly. WalletChip left isMenuOpen true when disabled turned on, so re-enabling the chip later reopened the menu without a click. isUserRejectedSwitchChain only matched numeric code 4001, missing connectors that surface it as the string '4001'. Co-Authored-By: Claude <noreply@anthropic.com> On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
|
Round 8 follow-up, fixed in e14d1a1:
Leaving the remaining suppressed nit ( Re-requesting Copilot review. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (3)
packages/citizen-claim-widget/src/adapter.ts:575
- When SDK initialization fails for a supported chain (
!sdk), settingstatusto'not_connected'is misleading because an address is present and the wallet may be connected; it also prevents the UI from surfacing a meaningful failure mode. Prefer settingstatusto'error'and setting a user-facing error message (or throwing aCitizenClaimAdapterErrorthat ends up inhumanReadableError) so the widget can explain that it couldn’t initialize RPC/SDK clients for the chain.
// A personalized status read, but still address-only: no connected
// account or passed-down provider is required, only the address itself.
const sdk = createReadOnlySdkForChain(statusChainId)
if (!sdk) {
setStatus('not_connected')
return
}
packages/citizen-claim-widget/src/CitizenClaimWidget.tsx:444
- The
unsupported_chainmessage and the “Ready to claim”/amount block can render at the same time (because the second condition is driven byclaimablesByChain.length > 0). This produces conflicting status copy for an unsupported active chain. Make these branches mutually exclusive (e.g., excludeunsupported_chainfrom the second condition, or convert to anelse ifstructure) so unsupported-chain messaging is the sole status UI in that state.
{status === 'unsupported_chain' && (
<>
<Text secondary>
{chainId
? `Claiming isn't available on ${getChainName(chainId)}. Switch network to continue.`
: 'Switch to a supported network to claim daily G$'}
</Text>
</>
)}
{(status === 'eligible' || status === 'claiming' || claimablesByChain.length > 0) && (
<>
{/*
Declarative to claim status: an already_claimed wallet with
other chains still available must not read as "no claims
left" (the "Just a little longer" copy below is reserved for
when every chain has actually been claimed for the day).
*/}
<Text secondary>
{status === 'already_claimed' && claimablesByChain.length === 1
? `G$ Claim is still available on ${getChainName(claimablesByChain[0].chainId)}`
: status === 'already_claimed' && claimablesByChain.length > 1
? 'G$ Claim is still available on other chains'
: 'Ready to claim'}
</Text>
{displayAmount && <TokenAmount token="G$" amount={displayAmount} size="xl" />}
</>
)}
packages/citizen-claim-widget/src/CitizenClaimWidget.tsx:353
- The comment says there is “no known claimable chain to target yet” and that this “falls back to Celo”, but the code actually prefers
claimablesByChain[0]?.chainIdwhen available. Update the comment to reflect the actual behavior (prefer first claimable chain, otherwise Celo), or change the logic to always prefer Celo if that’s the intended default.
// The wallet is connected but on a chain citizen-sdk doesn't support at
// all, so there is no known claimable chain to target yet — fall back
// to Celo, the first preferred supported chain.
await actions.switchChain?.(claimablesByChain[0]?.chainId ?? SupportedChains.CELO)
…-chain UI, misleading sdk-init status, stale comment CitizenClaimWidget rendered the unsupported_chain message alongside the eligible/claiming/Ready-to-claim block whenever claimablesByChain wasn't empty, e.g. an unsupported active chain with claimable UBI still sitting on another chain — showing two contradictory status lines at once. adapter.ts's loadClaimStatus set status to not_connected when the read-only sdk failed to initialize for a supported chain, which is misleading once an address is already known and connected. Also corrected a switch_chain comment that no longer matched the code. Co-Authored-By: Claude <noreply@anthropic.com> On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
|
Round 9 came back with no new blocking comments, but fixed 2 of its 3 suppressed nits — both are real bugs, not polish (commit ed76c51):
Also fixed a stale comment ( Re-requesting Copilot review. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (4)
packages/citizen-claim-widget/src/adapter.ts:78
CitizenClaimAdapterErroris used withinstanceofchecks, but the constructor doesn’t set the prototype chain explicitly. In some TS/JS build targets (especially when downleveling),instanceof CitizenClaimAdapterErrorcan fail forErrorsubclasses. Consider addingObject.setPrototypeOf(this, new.target.prototype)(or the class prototype) in the constructor to make theinstanceofcheck reliable.
/** Resolves a supported chain id to its display name, falling back to the raw id. */
function getChainDisplayName(chainId: number): string {
return CHAIN_CONFIGS[chainId]?.name ?? `Chain ${chainId}`
}
/**
* Thrown for adapter-level failures whose message is already user-facing
* (e.g. naming the specific chain an action cannot run on). humanReadableError
* passes these through verbatim instead of remapping them to a generic string.
*/
class CitizenClaimAdapterError extends Error {
constructor(message: string) {
super(message)
this.name = 'CitizenClaimAdapterError'
}
}
packages/core/src/provider.tsx:156
- When only one override is provided (e.g.
addressOverridebut notchainIdOverride), the effect still subscribes to both events even though one handler becomes a no-op. This is minor overhead, but easy to avoid by conditionally registering only the listeners that can update state (e.g. only subscribe toaccountsChangedwhen!hasAddressOverride, and only tochainChangedwhen!hasChainIdOverride).
useEffect(() => {
if (hasAddressOverride && hasChainIdOverride) return
if (!resolvedProvider) return
const handleAccountsChanged = (accounts: string[]) => {
if (!hasAddressOverride) setTrackedAddress(accounts[0] ?? null)
}
const handleChainChanged = (newChainId: string) => {
if (!hasChainIdOverride) setTrackedChainId(parseInt(newChainId, 16))
}
resolvedProvider.on('accountsChanged', handleAccountsChanged)
resolvedProvider.on('chainChanged', handleChainChanged)
packages/core/src/provider.tsx:227
- If
wallet_switchEthereumChainfails due to “method not supported” or a connector-specific failure and there’s noswitchChainOverride, the code rethrows the raw provider error. That raw message often isn’t user-friendly and may leak low-level details into UI error surfaces. Consider normalizing non-user-rejection failures to a consistent, user-facing error (similar toSWITCH_CHAIN_UNAVAILABLE_ERROR) when no override is available, while still preserving the raw error for logging/debugging.
const switchChain = useCallback(
async (targetChainId: number) => {
if (resolvedProvider) {
try {
await resolvedProvider.request({
method: 'wallet_switchEthereumChain',
params: [{ chainId: `0x${targetChainId.toString(16)}` }],
})
return
} catch (err) {
if (!switchChainOverride || isUserRejectedSwitchChain(err)) throw err
}
}
if (!switchChainOverride) {
throw new Error(SWITCH_CHAIN_UNAVAILABLE_ERROR)
}
await switchChainOverride(targetChainId)
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx:50
- Applying opacity to the entire chip (including text) can reduce contrast below WCAG thresholds, especially on low-contrast themes, and can make the address hard to read. Consider using theme “disabled” tokens (e.g. muted text color/border color) or applying opacity only to non-text decoration while keeping text contrast compliant; also ensure the disabled state isn’t conveyed by opacity alone.
<XStack position="relative" alignItems="center" opacity={disabled ? 0.5 : 1}>
…es instead of swallowing them The outer catch in handlePrimaryAction only called onClaimError for primaryAction === 'claim', so a failed switch_chain (wallet rejects, chain not added, raw provider error with no integrator override to fall back to, etc.) produced no toast and no callback — the user saw nothing happen at all. Now it surfaces an error toast + onClaimError naming the specific chain that was targeted, per the "name the chain an action can't run on" requirement, without leaking the raw wallet/provider error text (still logged via console.error for debugging). On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
|
Round 10 Copilot review — 0 non-suppressed comments, 4 suppressed (low-confidence) findings. Triage:
Re-requesting Copilot review. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx:111
- User-facing text has a grammatical error: change "wallets session" to "wallet's session".
if (!onDisconnect) {
setDisconnectMessage('Disconnect should be done in your wallets session')
return
}
packages/citizen-claim-widget/src/adapter.ts:67
- There are now two chain-name sources (
getChainDisplayNamehere andgetChainNameinCitizenClaimWidget.tsx). This can lead to inconsistent chain naming between UI copy and adapter error messages as supported chains evolve. Consider centralizing chain display naming in a single shared utility (e.g., exporting one function from a common module used by both files) so all user-facing chain names remain consistent.
/** Resolves a supported chain id to its display name, falling back to the raw id. */
function getChainDisplayName(chainId: number): string {
return CHAIN_CONFIGS[chainId]?.name ?? `Chain ${chainId}`
}
…rethrow switch-chain error, fix grammar nit handleSwitchChain caught and swallowed its error entirely (only setting the adapter's own status/error), so CitizenClaimWidget's new switch_chain catch branch (added in 17faecf to name the failing chain in a toast/onClaimError) was unreachable dead code — the caller had no error to catch. Now rethrows after setting status/error, mirroring the existing handleClaim pattern, so both the inline banner and the toast/callback fire consistently. Also fixes a grammar typo in WalletChip's disconnect message ("wallets session" -> "wallet's session"). On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
|
Round 11 Copilot review — 1 real comment, 2 suppressed.
Re-requesting Copilot review. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
packages/citizen-claim-widget/src/adapter.ts:561
- This treats the custodial misconfiguration case (no configured
clientsByChainfound, sostatusChainId === null) asunsupported_chain, which will drive the UI toward a network-switch narrative even though custodial mode doesn't depend on the wallet network. ForisCustodialExecution && statusChainId === null, setstatus: 'error'(and a clear user-facing error) instead ofunsupported_chain, reservingunsupported_chainfor the non-custodial / active-chain-not-supported scenario.
const statusChainId = isCustodialExecution
? SUPPORTED_CHAINS.find((supportedChainId) => claimExecution?.clientsByChain[supportedChainId]) ??
null
: chainId
if (statusChainId === null || !isSupportedChain(statusChainId)) {
await auxiliaryReads
// Chain is known but unsupported (or custodial has no chain configured
// at all) — a distinct status from not_connected so the UI can show
// "switch chain" copy instead of misleadingly asking an already-connected
// wallet to connect. Clear personalized entitlement from whatever chain
// was previously active.
setAmount(null)
setNextClaimTime(null)
setStatus('unsupported_chain')
return
}
packages/citizen-claim-widget/src/CitizenClaimWidget.tsx:624
- This introduces a new hard-coded chain id (42220) for the fallback network pill. Since the codebase already uses
SupportedChains.CELOelsewhere (e.g. in switch-chain logic), prefer the shared constant here to avoid drift if the preferred/default chain changes.
chainId={chainId ?? fallbackChainId ?? 42220}
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx:66
- The interactive chip is an
XStackwithonPress,aria-label, andaria-disabled, but it likely renders as a non-interactive element (e.g. adiv) without keyboard semantics. To make this accessible, render it as a semantic button component (preferred), or addrole=\"button\",tabIndex={disabled ? -1 : 0}, and keyboard handlers (Enter/Space) consistent with the disabled state.
<XStack position="relative" alignItems="center" opacity={disabled ? 0.5 : 1}>
<XStack
gap="$2"
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius="$full"
borderWidth={1}
borderColor="$borderColor"
cursor={disabled ? 'not-allowed' : 'pointer'}
onPress={() => {
if (disabled) return
setDisconnectMessage(null)
setIsMenuOpen((open) => !open)
}}
aria-label="Wallet options"
aria-disabled={disabled}
>
…ut, fix custodial misconfig status, use shared chain constant - provider.tsx: some WalletConnect sessions never resolve or reject wallet_switchEthereumChain, so the request could hang switchChain forever and never reach the integrator override fallback. Races it against a 10s timeout, treated like any other rejection. - adapter.ts: when custodial execution has no configured client for any supported chain, this was reported as 'unsupported_chain', driving a "switch network" narrative the user has no way to act on (custodial mode has no wallet chain to switch). Now reports 'error' with a clear message instead, reserving 'unsupported_chain' for the real switchable case. - CitizenClaimWidget.tsx: replaced a hard-coded 42220 fallback chain id with the existing SupportedChains.CELO constant already used elsewhere. On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
|
Round 12 Copilot review — 1 real comment, 3 suppressed.
Re-requesting Copilot review. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx:64
- The clickable chip is rendered as an
XStackwithonPress+aria-*, but without explicit button semantics (e.g.role="button"/ keyboard activation). This can make the menu inaccessible to keyboard and some assistive-tech users. Prefer rendering this as an actualButton/pressable component from the UI kit, or addrole="button",tabIndex={0}, and anonKeyDownhandler for Enter/Space (and ensurearia-disabledis on an element with appropriate role).
<XStack position="relative" alignItems="center" opacity={disabled ? 0.5 : 1}>
<XStack
gap="$2"
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius="$full"
borderWidth={1}
borderColor="$borderColor"
cursor={disabled ? 'not-allowed' : 'pointer'}
onPress={() => {
if (disabled) return
setDisconnectMessage(null)
setIsMenuOpen((open) => !open)
}}
aria-label="Wallet options"
aria-disabled={disabled}
>
packages/citizen-claim-widget/src/adapter.ts:409
- This allocates new
publicClient/walletClientinstances on every refresh for every supported chain, which can become expensive asloadClaimStatus/loadClaimablesByChainrerun (and can also increase connection churn). Consider caching clients pertargetChainId(and peraddresswhere relevant) via auseMemo-managedMapor a ref-backed cache to reuse transports/clients across refreshes.
const createReadOnlyClientsForChain = useCallback(
(targetChainId: number) => {
if (!address) return null
if (isCustodialExecution) {
const configuredClients = claimExecution?.clientsByChain[targetChainId]
return configuredClients ? normalizeClientBundle(configuredClients) : null
}
const chain = CHAIN_CONFIGS[targetChainId]
const rpcUrl = chain?.rpcUrls.default.http[0]
if (!chain || !rpcUrl) return null
const publicClient = createPublicClient({ chain, transport: http(rpcUrl) })
const walletClient = createWalletClient({
account: address as `0x${string}`,
chain,
transport: http(rpcUrl),
})
return { publicClient, walletClient }
},
[address, claimExecution, isCustodialExecution, normalizeClientBundle],
)
…erride when falling back to AppKit modal Resolving instead of throwing let callers treat an opened network-selection modal as an already-completed chain switch, since the modal never confirms the switch synchronously. On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
|
Round 13 Copilot review — 1 real comment, 2 suppressed.
Re-requesting Copilot review. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
apps/superfluid-campaign-web/src/App.tsx:24
- This error text is thrown as a plain
Error, butCitizenClaimWidget’shumanReadableError()generally remaps unknownError.messagevalues to a generic fallback, so users may never see this specific guidance. To ensure this message is surfaced, either (a) updatehumanReadableError()to explicitly pass through this message (or a stable error code/prefix), or (b) throw an error whose message matches an existing “pass-through” path used by the widget (e.g., a recognizable substring thathumanReadableError()maps deterministically).
const SWITCH_CHAIN_MANUAL_SELECTION_ERROR = 'Select the network in the wallet dialog, then try again.'
apps/superfluid-campaign-web/src/App.tsx:95
- This error text is thrown as a plain
Error, butCitizenClaimWidget’shumanReadableError()generally remaps unknownError.messagevalues to a generic fallback, so users may never see this specific guidance. To ensure this message is surfaced, either (a) updatehumanReadableError()to explicitly pass through this message (or a stable error code/prefix), or (b) throw an error whose message matches an existing “pass-through” path used by the widget (e.g., a recognizable substring thathumanReadableError()maps deterministically).
if (!targetNetwork) {
await open({ view: 'Networks' })
throw new Error(SWITCH_CHAIN_MANUAL_SELECTION_ERROR)
}
try {
await switchNetwork(targetNetwork)
} catch {
await open({ view: 'Networks' })
throw new Error(SWITCH_CHAIN_MANUAL_SELECTION_ERROR)
}
packages/citizen-claim-widget/src/adapter.ts:409
- This recreates
publicClient/walletClientinstances on each call (andloadClaimablesByChaincalls this across all supported chains). Consider memoizing clients per(address, chainId)(e.g., via a ref-backed cache oruseMemokeyed byaddress+ supported chains) to avoid repeatedly constructing transports/clients during refreshes.
const createReadOnlyClientsForChain = useCallback(
(targetChainId: number) => {
if (!address) return null
if (isCustodialExecution) {
const configuredClients = claimExecution?.clientsByChain[targetChainId]
return configuredClients ? normalizeClientBundle(configuredClients) : null
}
const chain = CHAIN_CONFIGS[targetChainId]
const rpcUrl = chain?.rpcUrls.default.http[0]
if (!chain || !rpcUrl) return null
const publicClient = createPublicClient({ chain, transport: http(rpcUrl) })
const walletClient = createWalletClient({
account: address as `0x${string}`,
chain,
transport: http(rpcUrl),
})
return { publicClient, walletClient }
},
[address, claimExecution, isCustodialExecution, normalizeClientBundle],
)
…n over cross-chain claimables for non-custodial wallets primaryAction returned 'claim' whenever any supported chain had a claimable balance, even when the active wallet chain was unsupported and claimOnChain targets only the active chain for non-custodial execution. The button now correctly offers 'switch_chain' first in that case; custodial execution (which has no single active chain) keeps its existing precedence. On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
|
Round 14 Copilot review — 1 real comment, 3 suppressed.
Re-requesting Copilot review. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
packages/citizen-claim-widget/src/adapter.ts:409
- This creates new viem clients (and underlying transports) on every call per chain. Since
loadClaimablesByChainfans out acrossSUPPORTED_CHAINSand may rerun frequently (refreshes, state transitions), this can add avoidable overhead. Consider cachingpublicClient/walletClientpertargetChainId(e.g., auseMemo/useRefMap keyed by chainId + rpcUrl) and reusing them between calls, only invalidating whenaddress(for walletClient.account) or RPC URL changes.
const createReadOnlyClientsForChain = useCallback(
(targetChainId: number) => {
if (!address) return null
if (isCustodialExecution) {
const configuredClients = claimExecution?.clientsByChain[targetChainId]
return configuredClients ? normalizeClientBundle(configuredClients) : null
}
const chain = CHAIN_CONFIGS[targetChainId]
const rpcUrl = chain?.rpcUrls.default.http[0]
if (!chain || !rpcUrl) return null
const publicClient = createPublicClient({ chain, transport: http(rpcUrl) })
const walletClient = createWalletClient({
account: address as `0x${string}`,
chain,
transport: http(rpcUrl),
})
return { publicClient, walletClient }
},
[address, claimExecution, isCustodialExecution, normalizeClientBundle],
)
packages/superfluid-campaign-widget/src/components/shared/WalletChip.tsx:67
- The new disabled state visually dims the chip and blocks
onPress, but it still presents as an interactive control without a semantic disabled element. IfXStackrenders to a non-button element,aria-disabledalone may not fully convey/implement disabled behavior for assistive tech. Prefer using a semantic button/pressable component that supports a realdisabledprop (or ensure the underlying element has appropriate role/keyboard handling and is removed from the tab order when disabled), so disabled behavior is consistent across mouse/keyboard/screen readers.
<XStack position="relative" alignItems="center" opacity={disabled ? 0.5 : 1}>
<XStack
gap="$2"
alignItems="center"
paddingHorizontal="$3"
paddingVertical="$2"
borderRadius="$full"
borderWidth={1}
borderColor="$borderColor"
cursor={disabled ? 'not-allowed' : 'pointer'}
onPress={() => {
if (disabled) return
setDisconnectMessage(null)
setIsMenuOpen((open) => !open)
}}
aria-label="Wallet options"
aria-disabled={disabled}
>
|
Round 15 Copilot review — 1 real comment (false positive), 2 suppressed (repeats).
Re-requesting Copilot review. |
…get): connect via AppKit override, fix whitelist/default-chain precedence, resolve chain display names - Thread connectOverride through SuperfluidCampaignWidget -> CitizenClaimWidget so the internal claim button opens the AppKit modal when a connectOverride is supplied, instead of always falling back to the raw injected provider. - Reorder primaryAction precedence so not_whitelisted takes priority over any cross-chain claimables, and gate the widget's own whitelist card the same way, so an unverified wallet only ever gets the sign-message/face-verify redirect instead of also attempting claims on other chains. - Make XDC the default chain (DEFAULT_APPKIT_NETWORKS order and the switch-chain/fallback chain id defaults), replacing CELO. - Broaden and export getChainDisplayName from adapter.ts to resolve mainnet and base in addition to FUSE/CELO/XDC, and reuse it from CitizenClaimWidget.tsx instead of a separate, narrower local duplicate, so the unsupported-chain message shows a network name instead of a raw chain id. Co-Authored-By: Claude <noreply@anthropic.com> On-Behalf-Of: gdpatchagent[onecli] (yaskkeryodtdijpv)
Closes #164
Scope
Standalone
superfluid-campaign-webapp only, per requester sign-off. Redirect support for other environments (in-wallet embedded widget) is left as-is for now.Changes
chainIdOverride) instead of always assuming Celo.switchChainOverridefollows AppKit's ownuseAppKitNetwork()state and callsswitchNetwork()with the matchingAppKitNetworkdescriptor (looked up via the now-exportedDEFAULT_APPKIT_NETWORKS). Falls back to AppKit's own network-selection modal (open({ view: 'Networks' })) when there's no direct descriptor or when the programmatic switch throws.disconnectLabeloverride threaded throughCampaignHeader,LeaderboardView, andWalletChip.loadClaimablesByChainnow fetches claim entitlement for every supported chain (Fuse, Celo, XDC), not just the active one — using the wallet-bound SDK for the active chain and the SDK'scheckEntitlement({ chainOverride, publicClient })for the rest. This behavior is shared by both AppKit and direct WalletConnect/injected connectors.switchChain(targetChainId)instead of being a no-op, and defaults to the first chain that actually has a claimable amount rather than a hardcoded chain.<chain>" when another chain has a pending claim, and the "have to wait a little longer" copy is shown only when every chain's claim for the day is already done.Testing
npx turbo run build --filter=@goodwidget/superfluid-campaign-web...— 6/6 packages build clean (includes a fulltsctype-check for the app).npx turbo run lint --filter=@goodwidget/superfluid-campaign-web...— clean.