Add revoke-operator flow to AI Credits Manage tab with signed backend action and confirmation dialog#189
Conversation
There was a problem hiding this comment.
Copilot wasn't able to review any files in this pull request.
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: sirpy <671095+sirpy@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (3)
packages/ai-credits-widget/src/components/manage/BuyerOperatorCard.tsx:194
onAcceptalways callscloseDialog()viafinally, so the dialog closes even ifactions.revokeOperatorConsent()fails. That preventsGoodWidgetDialogfrom showing its built-in error state and can look like the revoke succeeded even when it didn’t. Close the dialog only after a successful revoke and let thrown errors keep the dialog open (so status can flip to error).
onAccept: async () => {
try {
await actions.revokeOperatorConsent()
} finally {
closeDialog()
packages/ai-credits-widget/src/mocked/chainClient.ts:91
AiCreditsChainClient.getBuyerAuthNoncetakes abuyerargument, butMockAiCreditsChainClient.getBuyerAuthNoncedoesn’t. Even though extra args are ignored at runtime, keeping the mock signature aligned avoids confusion and prevents future refactors from accidentally relying on an unused parameter.
async getBuyerAuthNonce(): Promise<bigint> {
return 0n
}
packages/ai-credits-widget/src/backendClient.ts:56
- This PR changes the signed request contract for existing operations by replacing the
timestampfield withnonce(e.g.,WithdrawPrincipalRequest, and similarlyChannelOperationRequest+ the EIP-712 types inbuyerSignatures.ts). That’s an API/contract change that will break older backends unless they’ve been updated in lockstep. Please confirm backend compatibility (or consider supporting bothtimestampandnonceduring a transition) since the PR/issue scope is primarily the revoke-operator flow.
export type WithdrawPrincipalRequest = {
amount: string
recipient: string
nonce: string
signature: string
| {operatorConsented && ( | ||
| <Button | ||
| size="sm" | ||
| variant="outline" | ||
| borderColor="$error" | ||
| alignSelf="flex-start" | ||
| {...compactButtonProps} | ||
| disabled={operatorConsentPending} | ||
| onPress={handleOpenRevokeDialog} | ||
| > | ||
| <ButtonText color="$error">Revoke Operator</ButtonText> | ||
| </Button> | ||
| )} |
There was a problem hiding this comment.
Revoke button not gated on the signer key - Gated only on operatorConsented.
Deep-link buyers (operator signature, no buyerPrvKey) can confirm the destructive dialog and get "Signer private key missing" as the payoff.
| renderAccept={(onPress, label) => ( | ||
| <Button onPress={onPress}> | ||
| <ButtonText>{label}</ButtonText> | ||
| </Button> | ||
| )} |
There was a problem hiding this comment.
Double-click replays the nonce.
WidgetDialog's renderAccept ignores GoodWidgetDialog's isPending, so the built-in disable guard is dropped.
Users could grant operator consent in
BuyerOperatorCardbut had no revoke path. This adds a deliberate revoke flow in the Manage tab: a red action button gated by current consent state, a confirmation dialog with bonus-impact warnings, and an end-to-end signed revoke request to the backend.Manage-tab UX: conditional revoke control
BuyerOperatorCard.operatorConsented === true.Confirmation gate via existing dialog primitives
createDialog+GoodWidgetDialog), without introducing new UI primitives.Runtime/action plumbing for revoke
revokeOperatorConsentto widget action contract and adapter action surface.falseon success,Backend client integration
backendClient.POST /v1/accounts/:account/operator-revokecall with{ nonce, signature }.Story/QA fixture updates
revokeOperatorConsent.