[DRAFT][PLAN] AI Credits widget: "Revoke Operator" button with confirmation dialog
Plan for #154.
Reference files mapped (existing code to build on)
| File |
Role |
packages/ai-credits-widget/src/components/manage/BuyerOperatorCard.tsx |
UI insertion point. Current "Sign Consent"/"Consented" Button lives at ~L195-211, driven by operatorConsented/operatorConsentPending props and actions.signOperatorConsent(). |
packages/ai-credits-widget/src/operatorConsent.ts |
Consent payload/signing logic. buildSetOperatorPayload (~L49-78) and signOperatorConsentFromTypedData (~L80-101) are the pattern to mirror for a revoke payload. |
packages/ai-credits-widget/src/widgetRuntimeContract.ts |
Adapter contract. operatorConsented/operatorConsentPending/operatorAddress state (~L45-48) and the actions interface (~L63-96, signOperatorConsent at L85) — add revokeOperatorConsent here. |
packages/ai-credits-widget/src/backendClient.ts |
HTTP client. submitOperatorConsent (~L412-429) posts to ${accountBase(buyer)}/operator-consent via readBridgeResponseBody; OperatorConsentRequest type (~L81-84); AiCreditsBackendClient interface (~L204-230). |
packages/ai-credits-widget/src/adapter.ts |
Wires runtime-contract actions to backend calls (consent handler ~L1688 area) — add a handleRevokeOperatorConsent counterpart. |
packages/ai-credits-widget/src/mocked/backendClient.ts |
Mock backend used by Storybook fixtures/tests — needs a mock revoke implementation so stories/tests can exercise the new state. |
packages/ui/src/components/Dialog.tsx |
Reusable imperative dialog: createDialog(config) / updateDialogStatus / closeDialog (~L45-60). DialogConfig (~L15-28) takes title, body, acceptLabel, rejectLabel, onAccept (may return a Promise, auto-drives pending/success/error status), onReject. No new dialog component needed. |
packages/ui/src/components/Button.tsx |
No built-in "danger" variant (primary/secondary/outline/ghost/pill/text/list, ~L56-108). Existing red-button precedent: packages/streaming-widget PoolCard.tsx (~L103-109) uses variant="secondary" + borderColor="$error" + <ButtonText color="$error">. Reuse this pattern — no new UI primitive. |
tests/widgets/ai-credits-widget/states.spec.ts |
Existing Playwright spec, ~18 states already covered, baseline screenshots in tests/widgets/ai-credits-widget/test-results/. Extend per repo convention (memory: every GoodWidget task needs this spec + committed baselines). |
Existing @goodwidget packages to import
@goodwidget/ui — already the source of Button, ButtonText, and the createDialog/closeDialog/updateDialogStatus Dialog API; no new import needed beyond what BuyerOperatorCard.tsx already pulls in (L2).
@goodwidget/core — already a dependency of ai-credits-widget; no new usage anticipated unless the revoke action needs a shared error/toast utility already living there (verify during implementation).
New components assessed
No new components required — reusable-package placement is not applicable here. The button is a styled Button instance (existing variant + color token), and the dialog reuses the existing generic Dialog.tsx imperative API. Nothing here is generic enough to warrant new packages/ui primitives, and nothing here is specific enough to need a widget-local new component beyond editing BuyerOperatorCard.tsx in place.
Required states, flows, and behaviors
operatorConsented === true → "Revoke Operator" button visible, styled red (borderColor="$error", ButtonText color="$error").
operatorConsented === false → button not rendered.
- Click "Revoke Operator" →
createDialog(...) opens a confirmation dialog; no revoke call made yet.
- Dialog copy must state plainly: revoking removes the operator's ability to act on your behalf; any bonus balance will be deducted; any active stream bonuses will stop.
- Confirm (
onAccept) → fires actions.revokeOperatorConsent(). Dialog's built-in async status (pending → success/error) drives the spinner/check/alert icon; on success the card re-renders with operatorConsented: false (mirrors today's consent-grant success update).
- Cancel/dismiss (
onReject or close) → no state change, operator remains active, dialog closes.
- Revoke failure → surfaced via the dialog's existing
error status affordance, consistent with how BuyerOperatorCard.tsx already surfaces consent errors — no new error-handling pattern introduced.
- Cross-repo dependency:
revokeOperatorConsent ultimately needs a live backend endpoint to call. That endpoint is the companion GoodDollar/antseed-integration#22 bounty (POST /v1/accounts/:buyer/operator-reset, planned separately). If that endpoint isn't merged/deployed yet when this is implemented, the backend call in backendClient.ts/mocked/backendClient.ts should be written and tested against the mock immediately, with the real endpoint wired in as soon as it's available — do not block this widget's UI/dialog work on the backend bounty's timeline, but do not fabricate a fake endpoint path either; use the path specified in the antseed-integration plan (POST /v1/accounts/:buyer/operator-reset) so both land in sync.
Execution plan
operatorConsent.ts — add a revoke payload/signing counterpart to buildSetOperatorPayload/signOperatorConsentFromTypedData if the reset endpoint ends up requiring a buyer-signed payload (per the antseed-integration plan's contract findings, the on-chain reset call is owner-initiated, not buyer-signed — see antseed-integration plan's "Required states, flows, and behaviors" — so this step may reduce to: no new signing payload needed, revoke request just needs buyer address). Confirm against the final antseed-integration plan before implementing; do not assume a signature is required.
widgetRuntimeContract.ts — add revokeOperatorConsent: () => Promise<void> to the adapter actions interface, alongside signOperatorConsent (~L85).
backendClient.ts — add a revokeOperatorConsent(buyer: string) method to AiCreditsBackendClient (interface + real implementation) that POSTs to ${accountBase(buyer)}/operator-reset, following the exact shape of submitOperatorConsent (~L412-429) and using readBridgeResponseBody.
mocked/backendClient.ts — add the matching mock implementation so Storybook fixtures and the new Playwright spec can exercise revoke without a live backend.
adapter.ts — implement handleRevokeOperatorConsent, wiring the new runtime-contract action to backendClient.revokeOperatorConsent, updating operatorConsented/operatorAddress state on success.
BuyerOperatorCard.tsx — add the red "Revoke Operator" Button (visible only when operatorConsented), wire its onPress to createDialog({...}) with the required warning copy, onAccept: () => actions.revokeOperatorConsent().
- Tests — extend
tests/widgets/ai-credits-widget/states.spec.ts with states for: button visibility (consented vs not), dialog open/cancel, dialog confirm → success, dialog confirm → error. Commit new baseline screenshots under tests/widgets/ai-credits-widget/test-results/ per repo convention.
- Cross-reference — link this PR to
antseed-integration#22's PR once both exist, since the backend endpoint path/auth model is a shared contract between them.
Acceptance criteria
Human-reviewer checklist
[DRAFT][PLAN] AI Credits widget: "Revoke Operator" button with confirmation dialog
Plan for #154.
Reference files mapped (existing code to build on)
packages/ai-credits-widget/src/components/manage/BuyerOperatorCard.tsxButtonlives at ~L195-211, driven byoperatorConsented/operatorConsentPendingprops andactions.signOperatorConsent().packages/ai-credits-widget/src/operatorConsent.tsbuildSetOperatorPayload(~L49-78) andsignOperatorConsentFromTypedData(~L80-101) are the pattern to mirror for a revoke payload.packages/ai-credits-widget/src/widgetRuntimeContract.tsoperatorConsented/operatorConsentPending/operatorAddressstate (~L45-48) and the actions interface (~L63-96,signOperatorConsentat L85) — addrevokeOperatorConsenthere.packages/ai-credits-widget/src/backendClient.tssubmitOperatorConsent(~L412-429) posts to${accountBase(buyer)}/operator-consentviareadBridgeResponseBody;OperatorConsentRequesttype (~L81-84);AiCreditsBackendClientinterface (~L204-230).packages/ai-credits-widget/src/adapter.tshandleRevokeOperatorConsentcounterpart.packages/ai-credits-widget/src/mocked/backendClient.tspackages/ui/src/components/Dialog.tsxcreateDialog(config)/updateDialogStatus/closeDialog(~L45-60).DialogConfig(~L15-28) takestitle,body,acceptLabel,rejectLabel,onAccept(may return aPromise, auto-drives pending/success/error status),onReject. No new dialog component needed.packages/ui/src/components/Button.tsxprimary/secondary/outline/ghost/pill/text/list, ~L56-108). Existing red-button precedent:packages/streaming-widgetPoolCard.tsx(~L103-109) usesvariant="secondary"+borderColor="$error"+<ButtonText color="$error">. Reuse this pattern — no new UI primitive.tests/widgets/ai-credits-widget/states.spec.tstests/widgets/ai-credits-widget/test-results/. Extend per repo convention (memory: every GoodWidget task needs this spec + committed baselines).Existing
@goodwidgetpackages to import@goodwidget/ui— already the source ofButton,ButtonText, and thecreateDialog/closeDialog/updateDialogStatusDialog API; no new import needed beyond whatBuyerOperatorCard.tsxalready pulls in (L2).@goodwidget/core— already a dependency ofai-credits-widget; no new usage anticipated unless the revoke action needs a shared error/toast utility already living there (verify during implementation).New components assessed
No new components required — reusable-package placement is not applicable here. The button is a styled
Buttoninstance (existing variant + color token), and the dialog reuses the existing genericDialog.tsximperative API. Nothing here is generic enough to warrant newpackages/uiprimitives, and nothing here is specific enough to need a widget-local new component beyond editingBuyerOperatorCard.tsxin place.Required states, flows, and behaviors
operatorConsented === true→ "Revoke Operator" button visible, styled red (borderColor="$error",ButtonText color="$error").operatorConsented === false→ button not rendered.createDialog(...)opens a confirmation dialog; no revoke call made yet.onAccept) → firesactions.revokeOperatorConsent(). Dialog's built-in async status (pending → success/error) drives the spinner/check/alert icon; on success the card re-renders withoperatorConsented: false(mirrors today's consent-grant success update).onRejector close) → no state change, operator remains active, dialog closes.errorstatus affordance, consistent with howBuyerOperatorCard.tsxalready surfaces consent errors — no new error-handling pattern introduced.revokeOperatorConsentultimately needs a live backend endpoint to call. That endpoint is the companionGoodDollar/antseed-integration#22bounty (POST /v1/accounts/:buyer/operator-reset, planned separately). If that endpoint isn't merged/deployed yet when this is implemented, the backend call inbackendClient.ts/mocked/backendClient.tsshould be written and tested against the mock immediately, with the real endpoint wired in as soon as it's available — do not block this widget's UI/dialog work on the backend bounty's timeline, but do not fabricate a fake endpoint path either; use the path specified in the antseed-integration plan (POST /v1/accounts/:buyer/operator-reset) so both land in sync.Execution plan
operatorConsent.ts— add a revoke payload/signing counterpart tobuildSetOperatorPayload/signOperatorConsentFromTypedDataif the reset endpoint ends up requiring a buyer-signed payload (per the antseed-integration plan's contract findings, the on-chain reset call is owner-initiated, not buyer-signed — see antseed-integration plan's "Required states, flows, and behaviors" — so this step may reduce to: no new signing payload needed, revoke request just needsbuyeraddress). Confirm against the final antseed-integration plan before implementing; do not assume a signature is required.widgetRuntimeContract.ts— addrevokeOperatorConsent: () => Promise<void>to the adapter actions interface, alongsidesignOperatorConsent(~L85).backendClient.ts— add arevokeOperatorConsent(buyer: string)method toAiCreditsBackendClient(interface + real implementation) that POSTs to${accountBase(buyer)}/operator-reset, following the exact shape ofsubmitOperatorConsent(~L412-429) and usingreadBridgeResponseBody.mocked/backendClient.ts— add the matching mock implementation so Storybook fixtures and the new Playwright spec can exercise revoke without a live backend.adapter.ts— implementhandleRevokeOperatorConsent, wiring the new runtime-contract action tobackendClient.revokeOperatorConsent, updatingoperatorConsented/operatorAddressstate on success.BuyerOperatorCard.tsx— add the red "Revoke Operator"Button(visible only whenoperatorConsented), wire itsonPresstocreateDialog({...})with the required warning copy,onAccept: () => actions.revokeOperatorConsent().tests/widgets/ai-credits-widget/states.spec.tswith states for: button visibility (consented vs not), dialog open/cancel, dialog confirm → success, dialog confirm → error. Commit new baseline screenshots undertests/widgets/ai-credits-widget/test-results/per repo convention.antseed-integration#22's PR once both exist, since the backend endpoint path/auth model is a shared contract between them.Acceptance criteria
$errortoken) and only visible whenoperatorConsented === true.operator-resetendpoint and updatesoperatorConsented/operatorAddressstate on success.Button/Dialogcomponents and the$errorcolor token — no new UI primitives added topackages/uior the widget.tests/widgets/ai-credits-widget/states.spec.tscovers the new button/dialog states with committed baseline screenshots.Human-reviewer checklist
antseed-integration#22'soperator-resetendpoint (buyer address only, vs. a signed payload) before merging step 1/3 above — this plan currently assumes no buyer signature is required based on the antseed-integration contract findings (transferBuyerOperatorisonlyOwner, nobuyerSigparam), but re-verify against that bounty's final implementation.streaming-widgetPoolCard.tsxprecedent and reads consistently with the rest ofBuyerOperatorCard.tsx.