From d7905c48d612236a088db00a39a2f22c5286dd5c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 29 Jul 2026 13:14:32 +0000
Subject: [PATCH 1/8] Initial plan
From 8c9cb2ab63ba40008d48d4ee7e2a16a5e113a4d4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 29 Jul 2026 13:23:23 +0000
Subject: [PATCH 2/8] fix: prepare AI Credits widget host integration
Co-authored-by: L03TJ3 <6606028+L03TJ3@users.noreply.github.com>
---
packages/ai-credits-widget/README.md | 25 +++++++
packages/ai-credits-widget/package.json | 8 +++
.../ai-credits-widget/src/AiCreditsWidget.tsx | 1 -
packages/ai-credits-widget/src/adapter.ts | 36 +++++-----
.../ai-credits-widget/src/backendClient.ts | 71 +++++++++++++++----
packages/ai-credits-widget/src/chainClient.ts | 10 +--
packages/ai-credits-widget/src/index.ts | 1 +
packages/ai-credits-widget/src/register.ts | 14 ++--
packages/embed/src/createMiniAppElement.tsx | 13 ++--
9 files changed, 130 insertions(+), 49 deletions(-)
create mode 100644 packages/ai-credits-widget/README.md
diff --git a/packages/ai-credits-widget/README.md b/packages/ai-credits-widget/README.md
new file mode 100644
index 00000000..a189904a
--- /dev/null
+++ b/packages/ai-credits-widget/README.md
@@ -0,0 +1,25 @@
+# AI Credits Widget
+
+```tsx
+import { AiCreditsWidget } from '@goodwidget/ai-credits-widget'
+
+
+```
+
+For a Custom Element, import `@goodwidget/ai-credits-widget/register`. This import is safe during
+Node.js and Next.js server evaluation; it registers `` only in a browser. Set
+the injected EIP-1193 provider and optional `themeOverrides` as element properties.
+
+Production requires `backendUrl`. The widget keeps its Celo and Base RPC and contract defaults
+internally and fails closed with a backend-unavailable state when the production backend is absent.
+Only the `development` environment may use the built-in mock backend and chain clients.
+
+## EIP-1193 capabilities
+
+Grant only these EIP-1193 methods to the injected provider:
+
+- `eth_accounts` and `eth_chainId` to read the connected wallet.
+- `eth_requestAccounts` when the user selects **Connect Wallet**.
+- `wallet_switchEthereumChain` when the user selects the Celo network switch action.
+- `personal_sign` when the user generates the buyer key.
+- `eth_sendTransaction` when the user confirms a Celo payment.
diff --git a/packages/ai-credits-widget/package.json b/packages/ai-credits-widget/package.json
index 1ba6b91b..44b3262e 100644
--- a/packages/ai-credits-widget/package.json
+++ b/packages/ai-credits-widget/package.json
@@ -2,10 +2,18 @@
"name": "@goodwidget/ai-credits-widget",
"version": "0.1.0",
"description": "GoodWidget for buying AI coding credits with G$ on Celo",
+ "files": [
+ "dist",
+ "README.md"
+ ],
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
+ "sideEffects": [
+ "./dist/register.js",
+ "./dist/register.cjs"
+ ],
"exports": {
".": {
"types": "./dist/index.d.ts",
diff --git a/packages/ai-credits-widget/src/AiCreditsWidget.tsx b/packages/ai-credits-widget/src/AiCreditsWidget.tsx
index 81c75c2e..450ab150 100644
--- a/packages/ai-credits-widget/src/AiCreditsWidget.tsx
+++ b/packages/ai-credits-widget/src/AiCreditsWidget.tsx
@@ -18,7 +18,6 @@ import {
} from '@goodwidget/ui'
import { useAiCreditsAdapter } from './adapter'
import { useAiCreditsHistory } from './useAiCreditsHistory'
-import { DEPOSIT_BONUS_PERCENT, STREAM_BONUS_PERCENT } from './quoteMath'
import {
AiCreditsHero,
AiCreditsFlowStepper,
diff --git a/packages/ai-credits-widget/src/adapter.ts b/packages/ai-credits-widget/src/adapter.ts
index 0c2283f0..04e3951e 100644
--- a/packages/ai-credits-widget/src/adapter.ts
+++ b/packages/ai-credits-widget/src/adapter.ts
@@ -110,16 +110,6 @@ const WALLET_LOADING_STATE: Partial = {
operatorAddress: null,
}
-function isInBuyFlowStatus(status: AiCreditsWidgetStatus): boolean {
- return (
- status === 'purchase_setup' ||
- status === 'quote_ready' ||
- status === 'payment_pending' ||
- status === 'payment_confirmed' ||
- status === 'payment_failed'
- )
-}
-
function isNonBuyTab(tab: AiCreditsWidgetTab): boolean {
return tab === 'manage' || tab === 'history'
}
@@ -341,6 +331,10 @@ export function useAiCreditsAdapter({
}: UseAiCreditsAdapterOptions): AiCreditsWidgetAdapterResult {
const { address, chainId, isConnected, provider, connect } = useWallet()
const [state, setState] = useState(INITIAL_STATE)
+ const configurationError =
+ environment === 'development' || backendUrl
+ ? null
+ : 'AI Credits backend is not configured'
const providerRef = useRef(null)
providerRef.current = provider as EIP1193Provider | null
@@ -349,20 +343,20 @@ export function useAiCreditsAdapter({
const celoVault = vaultAddress ?? CELO_GD_ANTSEED_VAULT_FALLBACK
const backendClient = useMemo(
- () => createBackendClient(backendUrl),
- [backendUrl],
+ () => createBackendClient(backendUrl, environment),
+ [backendUrl, environment],
)
const chainClient = useMemo(
() =>
- createChainClient(backendUrl, {
+ createChainClient({
baseRpcUrl,
celoRpcUrl,
fundingVaultAddress,
celoVaultAddress: celoVault,
celoGoodIdAddress: goodIdAddress ?? CELO_GOODID_ADDRESS,
- }),
- [backendUrl, baseRpcUrl, celoRpcUrl, fundingVaultAddress, celoVault, goodIdAddress],
+ }, environment),
+ [environment, baseRpcUrl, celoRpcUrl, fundingVaultAddress, celoVault, goodIdAddress],
)
useEffect(() => {
@@ -370,6 +364,16 @@ export function useAiCreditsAdapter({
setState((prev) => (prev.status === 'connecting' ? prev : { ...INITIAL_STATE }))
return
}
+ if (configurationError) {
+ setState((prev) => ({
+ ...prev,
+ address,
+ chainId,
+ status: 'backend_unavailable',
+ error: configurationError,
+ }))
+ return
+ }
let cancelled = false
const sessionPatch = patchPayerSessionFields(address)
@@ -511,7 +515,7 @@ export function useAiCreditsAdapter({
return () => {
cancelled = true
}
- }, [isConnected, address, chainId, backendClient, chainClient, celoVault])
+ }, [isConnected, address, chainId, backendClient, chainClient, celoVault, configurationError])
const handleConnect = useCallback(async () => {
setState((prev) => withDerivedStatus(prev, { status: 'connecting', error: null }, false))
diff --git a/packages/ai-credits-widget/src/backendClient.ts b/packages/ai-credits-widget/src/backendClient.ts
index 7d7c0871..7db5d8ca 100644
--- a/packages/ai-credits-widget/src/backendClient.ts
+++ b/packages/ai-credits-widget/src/backendClient.ts
@@ -490,10 +490,7 @@ export class MockAiCreditsBackendClient implements AiCreditsBackendClient {
return { txHash, events: [entry] }
}
- async waitForSettlement(
- ref: AccountRef,
- _options: { txHashes?: string[]; previousBalance?: string } = {},
- ): Promise {
+ async waitForSettlement(ref: AccountRef): Promise {
await sleep(MOCK_DELAY_MS)
const state = this.getState(ref.payer)
for (const entry of state.transactions) {
@@ -505,10 +502,7 @@ export class MockAiCreditsBackendClient implements AiCreditsBackendClient {
return { totalCreditUsd: totalCreditUsdFromProfile(this.buildProfile(ref.payer)) }
}
- async closeChannel(
- channelId: string,
- _body: ChannelOperationRequest = {},
- ): Promise {
+ async closeChannel(channelId: string): Promise {
await sleep(MOCK_DELAY_MS)
return { channelId, action: 'close', bridge: { enabled: true, txHash: '0xmock' } }
}
@@ -525,10 +519,7 @@ export class MockAiCreditsBackendClient implements AiCreditsBackendClient {
}
}
- async submitOperatorConsent(
- buyer: string,
- _body: OperatorConsentRequest,
- ): Promise {
+ async submitOperatorConsent(buyer: string): Promise {
await sleep(MOCK_DELAY_MS)
const normalizedBuyer = normalizeAddress(buyer)
markMockOperatorConsent(normalizedBuyer)
@@ -710,10 +701,62 @@ export class ProductionAiCreditsBackendClient implements AiCreditsBackendClient
}
}
-export function createBackendClient(backendUrl: string | undefined): AiCreditsBackendClient {
- if (!backendUrl) {
+export class UnavailableAiCreditsBackendClient implements AiCreditsBackendClient {
+ private unavailable(): never {
+ throw new Error('AI Credits backend is not configured')
+ }
+
+ async getDiscountConfig() {
+ return this.unavailable()
+ }
+
+ async getAccountCredit() {
+ return this.unavailable()
+ }
+
+ async getCreditHistory() {
+ return this.unavailable()
+ }
+
+ async getOutstanding() {
+ return this.unavailable()
+ }
+
+ async getTransactions() {
+ return this.unavailable()
+ }
+
+ async notifyPayment() {
+ return this.unavailable()
+ }
+
+ async waitForSettlement() {
+ return this.unavailable()
+ }
+
+ async closeChannel() {
+ return this.unavailable()
+ }
+
+ async withdrawCredits() {
+ return this.unavailable()
+ }
+
+ async submitOperatorConsent() {
+ return this.unavailable()
+ }
+}
+
+export function createBackendClient(
+ backendUrl: string | undefined,
+ environment: 'production' | 'staging' | 'development' = 'production',
+): AiCreditsBackendClient {
+ if (!backendUrl && environment === 'development') {
return new MockAiCreditsBackendClient()
}
+ if (!backendUrl) {
+ return new UnavailableAiCreditsBackendClient()
+ }
return new ProductionAiCreditsBackendClient(backendUrl)
}
diff --git a/packages/ai-credits-widget/src/chainClient.ts b/packages/ai-credits-widget/src/chainClient.ts
index 02571ebf..a3a53a0e 100644
--- a/packages/ai-credits-widget/src/chainClient.ts
+++ b/packages/ai-credits-widget/src/chainClient.ts
@@ -6,7 +6,7 @@ import {
type Chain,
type PublicClient,
} from 'viem'
-import type { AiCreditsQuote } from './widgetRuntimeContract'
+import type { AiCreditsQuote, AiCreditsWidgetEnvironment } from './widgetRuntimeContract'
import type { BuyerOperatorStatus, OperatorConsentPayloadResponse } from './operatorConsent'
import { ANTSEED_DEPOSITS_BASE_ADDRESS, buildSetOperatorPayload } from './operatorConsent'
import type { AccountRef } from './backendTypes'
@@ -254,7 +254,7 @@ export class MockAiCreditsChainClient implements AiCreditsChainClient {
this.goodIdVerified = options.goodIdVerified ?? false
}
- async isGoodIdVerified(_account: string): Promise {
+ async isGoodIdVerified(): Promise {
return this.goodIdVerified
}
@@ -308,16 +308,16 @@ export class MockAiCreditsChainClient implements AiCreditsChainClient {
}
}
- async getWithdrawableUsd(_buyer: string): Promise {
+ async getWithdrawableUsd(): Promise {
return '0'
}
}
export function createChainClient(
- backendUrl: string | undefined,
options: AiCreditsChainClientOptions = {},
+ environment: AiCreditsWidgetEnvironment = 'production',
): AiCreditsChainClient {
- if (!backendUrl) {
+ if (environment === 'development') {
return new MockAiCreditsChainClient()
}
return new ProductionAiCreditsChainClient({
diff --git a/packages/ai-credits-widget/src/index.ts b/packages/ai-credits-widget/src/index.ts
index 2eed7f08..ee429068 100644
--- a/packages/ai-credits-widget/src/index.ts
+++ b/packages/ai-credits-widget/src/index.ts
@@ -28,6 +28,7 @@ export type {
export {
MockAiCreditsBackendClient,
ProductionAiCreditsBackendClient,
+ UnavailableAiCreditsBackendClient,
createBackendClient,
buildAccountView,
enrichAccountView,
diff --git a/packages/ai-credits-widget/src/register.ts b/packages/ai-credits-widget/src/register.ts
index e4743b5c..9ebf3662 100644
--- a/packages/ai-credits-widget/src/register.ts
+++ b/packages/ai-credits-widget/src/register.ts
@@ -1,5 +1,3 @@
-import { AiCreditsWidgetElement } from './element'
-
const DEFAULT_TAG_NAME = 'ai-credits-widget'
/**
@@ -11,18 +9,20 @@ const DEFAULT_TAG_NAME = 'ai-credits-widget'
* Then use in HTML:
*
*
- * Returns the tag name so you can use it programmatically:
- * const tag = register() // 'ai-credits-widget'
+ * Resolves to the tag name so you can use it programmatically:
+ * const tag = await register() // 'ai-credits-widget'
*
* Or register under a custom tag:
- * const tag = register('my-ai-credits-widget')
+ * const tag = await register('my-ai-credits-widget')
*/
-export function register(tagName: string = DEFAULT_TAG_NAME): string {
+export async function register(tagName: string = DEFAULT_TAG_NAME): Promise {
+ // Keep browser-only Custom Element dependencies out of server evaluation.
if (typeof customElements === 'undefined') return tagName
if (!customElements.get(tagName)) {
+ const { AiCreditsWidgetElement } = await import('./element')
customElements.define(tagName, AiCreditsWidgetElement)
}
return tagName
}
-register()
+void register()
diff --git a/packages/embed/src/createMiniAppElement.tsx b/packages/embed/src/createMiniAppElement.tsx
index 7357a07c..a5b7bed1 100644
--- a/packages/embed/src/createMiniAppElement.tsx
+++ b/packages/embed/src/createMiniAppElement.tsx
@@ -56,13 +56,10 @@ export function createMiniAppElement(
App: React.ComponentType>,
options: MiniAppElementOptions = {},
) {
- const HTMLElementBase =
+ const HTMLElementBase: typeof HTMLElement =
typeof globalThis !== 'undefined' && 'HTMLElement' in globalThis
? (globalThis as { HTMLElement: typeof HTMLElement }).HTMLElement
- : undefined
- if (!HTMLElementBase) {
- throw new Error('createMiniAppElement is only supported in DOM environments')
- }
+ : (class {} as typeof HTMLElement)
const {
shadow = true,
@@ -91,7 +88,7 @@ export function createMiniAppElement(
static get observedAttributes(): string[] {
return Object.entries(normalizedProps)
- .filter(([_, def]) => def.type === 'attribute')
+ .filter(([, def]) => def.type === 'attribute')
.map(([name]) => toKebabCase(name))
}
@@ -181,6 +178,10 @@ export function createMiniAppElement(
const appProps: Record = {
...this.#extraProps,
+ provider: this.#provider ?? undefined,
+ config: this.#config ?? defaultConfig,
+ themeOverrides: mergedOverrides,
+ defaultTheme,
}
for (const eventName of events) {
From 48516c489d06371287a3ddccbd7c3a685712124c Mon Sep 17 00:00:00 2001
From: blueogin
Date: Wed, 29 Jul 2026 14:53:57 -0400
Subject: [PATCH 3/8] refactor(ai-credits): streamline payment status handling
and enhance widget structure
- Simplified payment status checks in the adapter by removing redundant conditions for 'backend_unavailable'.
- Updated the logic for setting derived statuses to improve clarity and maintainability.
- Enhanced the AiCreditsWidgetElement structure by creating a base class for better property management.
- Introduced bridged property handling in createMiniAppElement to improve component integration and state management.
---
packages/ai-credits-widget/src/adapter.ts | 21 ++++++++----
packages/ai-credits-widget/src/element.ts | 9 ++++-
packages/embed/src/createMiniAppElement.tsx | 38 +++++++++++++++++++++
3 files changed, 60 insertions(+), 8 deletions(-)
diff --git a/packages/ai-credits-widget/src/adapter.ts b/packages/ai-credits-widget/src/adapter.ts
index 04e3951e..6cdf3a81 100644
--- a/packages/ai-credits-widget/src/adapter.ts
+++ b/packages/ai-credits-widget/src/adapter.ts
@@ -382,8 +382,7 @@ export function useAiCreditsAdapter({
if (
prev.status === 'payment_pending' ||
prev.status === 'payment_confirmed' ||
- prev.status === 'payment_failed' ||
- prev.status === 'backend_unavailable'
+ prev.status === 'payment_failed'
) {
return prev
}
@@ -905,7 +904,9 @@ export function useAiCreditsAdapter({
const statusSeed =
options?.afterGoodIdVerify && prev.status === 'payment_failed'
? 'quote_ready'
- : prev.status
+ : prev.status === 'backend_unavailable'
+ ? 'purchase_setup'
+ : prev.status
return withDerivedStatus(
{ ...prev, status: statusSeed },
{
@@ -1105,10 +1106,16 @@ export function useAiCreditsAdapter({
)
const handleRetry = useCallback(async () => {
- setState((prev) =>
- withDerivedStatus(prev, { activeTab: 'buy', status: 'purchase_setup', error: null }, true),
- )
- }, [])
+ if (configurationError) {
+ setState((prev) => ({
+ ...prev,
+ status: 'backend_unavailable',
+ error: configurationError,
+ }))
+ return
+ }
+ await handleRefresh()
+ }, [configurationError, handleRefresh])
const handleSetActiveTab = useCallback((tab: AiCreditsWidgetTab) => {
if (tab === 'buy') {
diff --git a/packages/ai-credits-widget/src/element.ts b/packages/ai-credits-widget/src/element.ts
index 6669e5cb..673a9d7e 100644
--- a/packages/ai-credits-widget/src/element.ts
+++ b/packages/ai-credits-widget/src/element.ts
@@ -17,11 +17,18 @@ import type React from 'react'
* el.backendUrl = 'https://api.antseed.xyz'
* el.themeOverrides = { tokens: { color: { primary: '#00AFFE' } } }
*/
-export const AiCreditsWidgetElement = createMiniAppElement(
+const AiCreditsWidgetElementBase = createMiniAppElement(
AiCreditsWidget as React.ComponentType>,
{
shadow: true,
defaultTheme: 'dark',
+ props: {
+ backendUrl: 'property',
+ },
events: ['pay-success', 'pay-error'],
},
)
+
+export class AiCreditsWidgetElement extends AiCreditsWidgetElementBase {
+ declare backendUrl: string | undefined
+}
diff --git a/packages/embed/src/createMiniAppElement.tsx b/packages/embed/src/createMiniAppElement.tsx
index a5b7bed1..8a125a4a 100644
--- a/packages/embed/src/createMiniAppElement.tsx
+++ b/packages/embed/src/createMiniAppElement.tsx
@@ -9,6 +9,9 @@ import { readCSSOverrides, observeCSSChanges } from './cssPropertyBridge'
import { normalizePropDefs, toKebabCase, toCamelCase, emitEvent } from './bridge'
import type { PropDefinitions } from './bridge'
+const getBridgedProperty = Symbol('getBridgedProperty')
+const setBridgedProperty = Symbol('setBridgedProperty')
+
export interface MiniAppElementOptions {
shadow?: boolean
props?: PropDefinitions
@@ -86,6 +89,18 @@ export function createMiniAppElement(
#disconnectStyleSync: (() => void) | null = null
#extraProps: Record = {}
+ constructor() {
+ super()
+ for (const [name, definition] of Object.entries(normalizedProps)) {
+ if (definition.type !== 'property' || !Object.prototype.hasOwnProperty.call(this, name)) {
+ continue
+ }
+ const value = (this as unknown as Record)[name]
+ delete (this as unknown as Record)[name]
+ this[setBridgedProperty](name, value)
+ }
+ }
+
static get observedAttributes(): string[] {
return Object.entries(normalizedProps)
.filter(([, def]) => def.type === 'attribute')
@@ -171,6 +186,15 @@ export function createMiniAppElement(
emitEvent(this, eventName, detail)
}
+ [getBridgedProperty](name: string): unknown {
+ return this.#extraProps[name]
+ }
+
+ [setBridgedProperty](name: string, value: unknown): void {
+ this.#extraProps[name] = value
+ this.#render()
+ }
+
#render() {
if (!this.#root) return
@@ -207,5 +231,19 @@ export function createMiniAppElement(
}
}
+ for (const [name, definition] of Object.entries(normalizedProps)) {
+ if (definition.type !== 'property' || name in GoodWidgetElement.prototype) continue
+ Object.defineProperty(GoodWidgetElement.prototype, name, {
+ configurable: true,
+ enumerable: true,
+ get(this: GoodWidgetElement) {
+ return this[getBridgedProperty](name)
+ },
+ set(this: GoodWidgetElement, value: unknown) {
+ this[setBridgedProperty](name, value)
+ },
+ })
+ }
+
return GoodWidgetElement
}
From a48f9825c292bf1fc18ca355df242b845a448927 Mon Sep 17 00:00:00 2001
From: blueogin
Date: Wed, 29 Jul 2026 16:53:07 -0400
Subject: [PATCH 4/8] refactor(embed): update MiniAppElement interfaces and
adjust createMiniAppElement return type
- Introduced new MiniAppElement and MiniAppElementConstructor interfaces to enhance type safety and clarity in the embed package.
- Changed the return type of createMiniAppElement to MiniAppElementConstructor for better integration and usage consistency.
- Updated index export to include the new types for improved accessibility in other modules.
---
.../src/components/history/HistoryTab.tsx | 2 +-
packages/embed/src/createMiniAppElement.tsx | 16 +++++++++++++++-
packages/embed/src/index.ts | 5 +++++
3 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/packages/ai-credits-widget/src/components/history/HistoryTab.tsx b/packages/ai-credits-widget/src/components/history/HistoryTab.tsx
index b7225d17..27e52b69 100644
--- a/packages/ai-credits-widget/src/components/history/HistoryTab.tsx
+++ b/packages/ai-credits-widget/src/components/history/HistoryTab.tsx
@@ -385,7 +385,7 @@ function StatusFilterSelect({
>,
options: MiniAppElementOptions = {},
-) {
+): MiniAppElementConstructor {
const HTMLElementBase: typeof HTMLElement =
typeof globalThis !== 'undefined' && 'HTMLElement' in globalThis
? (globalThis as { HTMLElement: typeof HTMLElement }).HTMLElement
diff --git a/packages/embed/src/index.ts b/packages/embed/src/index.ts
index 57fe388b..1396a556 100644
--- a/packages/embed/src/index.ts
+++ b/packages/embed/src/index.ts
@@ -1,4 +1,9 @@
export { createMiniAppElement } from './createMiniAppElement'
+export type {
+ MiniAppElement,
+ MiniAppElementConstructor,
+ MiniAppElementOptions,
+} from './createMiniAppElement'
export { DefaultAppKitProvider } from './DefaultAppKitProvider'
export { injectStylesIntoShadow, updateShadowStyles, getResetCSS } from './shadowStyles'
export { readCSSOverrides, observeCSSChanges } from './cssPropertyBridge'
From 6d44cbdd06ffef01bb159bd7b0716c15301681ed Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 29 Jul 2026 20:59:54 +0000
Subject: [PATCH 5/8] fix ai-credits review feedback for element upgrades and
client compatibility
Co-authored-by: blueogin <43612769+blueogin@users.noreply.github.com>
---
.../ai-credits-widget/src/AiCreditsWidget.tsx | 1 +
packages/ai-credits-widget/src/chainClient.ts | 28 +++++++++++++++++--
packages/ai-credits-widget/src/register.ts | 2 +-
.../src/useAiCreditsHistory.ts | 8 ++++--
packages/embed/src/createMiniAppElement.tsx | 25 +++++++++++++----
5 files changed, 53 insertions(+), 11 deletions(-)
diff --git a/packages/ai-credits-widget/src/AiCreditsWidget.tsx b/packages/ai-credits-widget/src/AiCreditsWidget.tsx
index 450ab150..a8f7ed8c 100644
--- a/packages/ai-credits-widget/src/AiCreditsWidget.tsx
+++ b/packages/ai-credits-widget/src/AiCreditsWidget.tsx
@@ -307,6 +307,7 @@ function AiCreditsInner({
const history = useAiCreditsHistory({
address: state.address,
backendUrl,
+ environment,
})
const handlePay = useCallback(
diff --git a/packages/ai-credits-widget/src/chainClient.ts b/packages/ai-credits-widget/src/chainClient.ts
index a3a53a0e..00c65370 100644
--- a/packages/ai-credits-widget/src/chainClient.ts
+++ b/packages/ai-credits-widget/src/chainClient.ts
@@ -314,9 +314,33 @@ export class MockAiCreditsChainClient implements AiCreditsChainClient {
}
export function createChainClient(
- options: AiCreditsChainClientOptions = {},
- environment: AiCreditsWidgetEnvironment = 'production',
+ backendUrl: string | undefined,
+ options?: AiCreditsChainClientOptions,
+ environment?: AiCreditsWidgetEnvironment,
+): AiCreditsChainClient
+export function createChainClient(
+ options?: AiCreditsChainClientOptions,
+ environment?: AiCreditsWidgetEnvironment,
+): AiCreditsChainClient
+export function createChainClient(
+ backendUrlOrOptions: string | undefined | AiCreditsChainClientOptions = {},
+ optionsOrEnvironment: AiCreditsChainClientOptions | AiCreditsWidgetEnvironment = {},
+ maybeEnvironment?: AiCreditsWidgetEnvironment,
): AiCreditsChainClient {
+ const usingLegacySignature =
+ typeof backendUrlOrOptions === 'string' ||
+ (backendUrlOrOptions === undefined &&
+ (typeof optionsOrEnvironment === 'object' || typeof optionsOrEnvironment === 'undefined'))
+ const backendUrl = usingLegacySignature ? backendUrlOrOptions : undefined
+ const options = (usingLegacySignature
+ ? typeof optionsOrEnvironment === 'object'
+ ? optionsOrEnvironment
+ : {}
+ : backendUrlOrOptions) as AiCreditsChainClientOptions
+ const environment: AiCreditsWidgetEnvironment = usingLegacySignature
+ ? (maybeEnvironment ?? (backendUrl ? 'production' : 'development'))
+ : (typeof optionsOrEnvironment === 'string' ? optionsOrEnvironment : 'production')
+
if (environment === 'development') {
return new MockAiCreditsChainClient()
}
diff --git a/packages/ai-credits-widget/src/register.ts b/packages/ai-credits-widget/src/register.ts
index 9ebf3662..46e2fc37 100644
--- a/packages/ai-credits-widget/src/register.ts
+++ b/packages/ai-credits-widget/src/register.ts
@@ -25,4 +25,4 @@ export async function register(tagName: string = DEFAULT_TAG_NAME): Promise undefined)
diff --git a/packages/ai-credits-widget/src/useAiCreditsHistory.ts b/packages/ai-credits-widget/src/useAiCreditsHistory.ts
index 9a23318a..49bba754 100644
--- a/packages/ai-credits-widget/src/useAiCreditsHistory.ts
+++ b/packages/ai-credits-widget/src/useAiCreditsHistory.ts
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import type { GdCreditEntry } from './backendTypes'
import { createBackendClient } from './backendClient'
+import type { AiCreditsWidgetEnvironment } from './widgetRuntimeContract'
export const HISTORY_PAGE_SIZE = 10
export const HISTORY_LOOKBACK_DAYS = 90
@@ -84,8 +85,9 @@ export interface UseAiCreditsHistoryResult {
export function useAiCreditsHistory(options: {
address: string | null
backendUrl?: string
+ environment?: AiCreditsWidgetEnvironment
}): UseAiCreditsHistoryResult {
- const { address, backendUrl } = options
+ const { address, backendUrl, environment = 'production' } = options
const defaultRange = useMemo(() => getLast90DaysRange(), [])
const [selectedSources, setSelectedSources] = useState(createDefaultSelectedSources)
@@ -130,7 +132,7 @@ export function useAiCreditsHistory(options: {
else setLoading(true)
setError(null)
- const client = createBackendClient(backendUrl)
+ const client = createBackendClient(backendUrl, environment)
const apiSource = activeSources.length === 1 ? activeSources[0] : undefined
const fundingStatus = statusFilter === 'all' ? undefined : statusFilter
@@ -160,7 +162,7 @@ export function useAiCreditsHistory(options: {
setLoadingMore(false)
}
},
- [address, backendUrl, activeSources, statusFilter, fromDate, toDate, selectedSources],
+ [address, backendUrl, environment, activeSources, statusFilter, fromDate, toDate, selectedSources],
)
useEffect(() => {
diff --git a/packages/embed/src/createMiniAppElement.tsx b/packages/embed/src/createMiniAppElement.tsx
index 6b86dcc4..db3fad0c 100644
--- a/packages/embed/src/createMiniAppElement.tsx
+++ b/packages/embed/src/createMiniAppElement.tsx
@@ -105,13 +105,28 @@ export function createMiniAppElement(
constructor() {
super()
- for (const [name, definition] of Object.entries(normalizedProps)) {
- if (definition.type !== 'property' || !Object.prototype.hasOwnProperty.call(this, name)) {
- continue
- }
+ const upgradeProperty = (name: string, apply: (value: unknown) => void) => {
+ if (!Object.prototype.hasOwnProperty.call(this, name)) return
const value = (this as unknown as Record)[name]
delete (this as unknown as Record)[name]
- this[setBridgedProperty](name, value)
+ apply(value)
+ }
+
+ upgradeProperty('provider', (value) => {
+ this.provider = (value as EIP1193Provider | null | undefined) ?? null
+ })
+ upgradeProperty('themeOverrides', (value) => {
+ this.themeOverrides = value as GoodWidgetThemeOverrides | undefined
+ })
+ upgradeProperty('config', (value) => {
+ this.config = value as GoodWidgetConfig | undefined
+ })
+
+ for (const [name, definition] of Object.entries(normalizedProps)) {
+ if (definition.type !== 'property') continue
+ upgradeProperty(name, (value) => {
+ this[setBridgedProperty](name, value)
+ })
}
}
From ac25a7a4d9a6b92547dc0beca8d3200330b1f1c2 Mon Sep 17 00:00:00 2001
From: blueogin
Date: Thu, 30 Jul 2026 18:01:03 -0400
Subject: [PATCH 6/8] feat(ai-credits-widget): add widget metadata for improved
integration
- Introduced `goodWidgetMetadata` containing package name and version for better identification and integration of the AI Credits widget.
---
packages/ai-credits-widget/src/register.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/packages/ai-credits-widget/src/register.ts b/packages/ai-credits-widget/src/register.ts
index 46e2fc37..472ee038 100644
--- a/packages/ai-credits-widget/src/register.ts
+++ b/packages/ai-credits-widget/src/register.ts
@@ -1,5 +1,10 @@
const DEFAULT_TAG_NAME = 'ai-credits-widget'
+export const goodWidgetMetadata = {
+ packageName: '@goodwidget/ai-credits-widget',
+ packageVersion: '0.1.0',
+} as const
+
/**
* Register the custom element.
*
From d78b1ce1a2664199b29ddad91e75cf55aeef69a0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 31 Jul 2026 09:16:43 +0000
Subject: [PATCH 7/8] refactor(ai-credits): simplify createChainClient to
environment API
Co-authored-by: L03TJ3 <6606028+L03TJ3@users.noreply.github.com>
---
packages/ai-credits-widget/src/adapter.ts | 2 +-
packages/ai-credits-widget/src/chainClient.ts | 30 ++-----------------
2 files changed, 4 insertions(+), 28 deletions(-)
diff --git a/packages/ai-credits-widget/src/adapter.ts b/packages/ai-credits-widget/src/adapter.ts
index 6cdf3a81..c6bbc5f4 100644
--- a/packages/ai-credits-widget/src/adapter.ts
+++ b/packages/ai-credits-widget/src/adapter.ts
@@ -355,7 +355,7 @@ export function useAiCreditsAdapter({
fundingVaultAddress,
celoVaultAddress: celoVault,
celoGoodIdAddress: goodIdAddress ?? CELO_GOODID_ADDRESS,
- }, environment),
+ }, environment === 'development' ? 'development' : 'production'),
[environment, baseRpcUrl, celoRpcUrl, fundingVaultAddress, celoVault, goodIdAddress],
)
diff --git a/packages/ai-credits-widget/src/chainClient.ts b/packages/ai-credits-widget/src/chainClient.ts
index 00c65370..310a8aa3 100644
--- a/packages/ai-credits-widget/src/chainClient.ts
+++ b/packages/ai-credits-widget/src/chainClient.ts
@@ -6,7 +6,7 @@ import {
type Chain,
type PublicClient,
} from 'viem'
-import type { AiCreditsQuote, AiCreditsWidgetEnvironment } from './widgetRuntimeContract'
+import type { AiCreditsQuote } from './widgetRuntimeContract'
import type { BuyerOperatorStatus, OperatorConsentPayloadResponse } from './operatorConsent'
import { ANTSEED_DEPOSITS_BASE_ADDRESS, buildSetOperatorPayload } from './operatorConsent'
import type { AccountRef } from './backendTypes'
@@ -314,33 +314,9 @@ export class MockAiCreditsChainClient implements AiCreditsChainClient {
}
export function createChainClient(
- backendUrl: string | undefined,
- options?: AiCreditsChainClientOptions,
- environment?: AiCreditsWidgetEnvironment,
-): AiCreditsChainClient
-export function createChainClient(
- options?: AiCreditsChainClientOptions,
- environment?: AiCreditsWidgetEnvironment,
-): AiCreditsChainClient
-export function createChainClient(
- backendUrlOrOptions: string | undefined | AiCreditsChainClientOptions = {},
- optionsOrEnvironment: AiCreditsChainClientOptions | AiCreditsWidgetEnvironment = {},
- maybeEnvironment?: AiCreditsWidgetEnvironment,
+ options: AiCreditsChainClientOptions = {},
+ environment: 'development' | 'production' = 'production',
): AiCreditsChainClient {
- const usingLegacySignature =
- typeof backendUrlOrOptions === 'string' ||
- (backendUrlOrOptions === undefined &&
- (typeof optionsOrEnvironment === 'object' || typeof optionsOrEnvironment === 'undefined'))
- const backendUrl = usingLegacySignature ? backendUrlOrOptions : undefined
- const options = (usingLegacySignature
- ? typeof optionsOrEnvironment === 'object'
- ? optionsOrEnvironment
- : {}
- : backendUrlOrOptions) as AiCreditsChainClientOptions
- const environment: AiCreditsWidgetEnvironment = usingLegacySignature
- ? (maybeEnvironment ?? (backendUrl ? 'production' : 'development'))
- : (typeof optionsOrEnvironment === 'string' ? optionsOrEnvironment : 'production')
-
if (environment === 'development') {
return new MockAiCreditsChainClient()
}
From ee3a1d55856da9908a528f7ac94b26df4692d3a2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 31 Jul 2026 09:39:26 +0000
Subject: [PATCH 8/8] refactor(ai-credits): isolate mocked runtime
Co-authored-by: L03TJ3 <6606028+L03TJ3@users.noreply.github.com>
---
.../helpers/aiCreditsWidgetStories.tsx | 3 +-
packages/ai-credits-widget/package.json | 5 +
.../ai-credits-widget/src/AiCreditsWidget.tsx | 10 +
packages/ai-credits-widget/src/adapter.ts | 39 ++-
.../ai-credits-widget/src/backendClient.ts | 291 +-----------------
packages/ai-credits-widget/src/chainClient.ts | 82 -----
packages/ai-credits-widget/src/index.ts | 1 -
.../src/mocked/AiCreditsMockWidget.tsx | 22 ++
.../src/mocked/backendClient.ts | 212 +++++++++++++
.../src/mocked/chainClient.ts | 88 ++++++
.../ai-credits-widget/src/mocked/index.ts | 3 +
.../src/useAiCreditsHistory.ts | 18 +-
.../src/widgetRuntimeContract.ts | 11 +
packages/ai-credits-widget/tsup.config.ts | 1 +
14 files changed, 398 insertions(+), 388 deletions(-)
create mode 100644 packages/ai-credits-widget/src/mocked/AiCreditsMockWidget.tsx
create mode 100644 packages/ai-credits-widget/src/mocked/backendClient.ts
create mode 100644 packages/ai-credits-widget/src/mocked/chainClient.ts
create mode 100644 packages/ai-credits-widget/src/mocked/index.ts
diff --git a/examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx b/examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx
index de260e3a..9ae1f66f 100644
--- a/examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx
+++ b/examples/storybook/src/stories/helpers/aiCreditsWidgetStories.tsx
@@ -7,6 +7,7 @@ import {
type AiCreditsWidgetAdapterState,
type AiCreditsWidgetStatus,
} from '@goodwidget/ai-credits-widget'
+import { MockAiCreditsWidget } from '@goodwidget/ai-credits-widget/mocked'
import {
DefaultAppKitProvider,
useAppKit,
@@ -295,7 +296,7 @@ export function MockBackendStory() {
return (
-
+
)
}
diff --git a/packages/ai-credits-widget/package.json b/packages/ai-credits-widget/package.json
index 44b3262e..86cfd8d1 100644
--- a/packages/ai-credits-widget/package.json
+++ b/packages/ai-credits-widget/package.json
@@ -29,6 +29,11 @@
"types": "./dist/register.d.ts",
"import": "./dist/register.js",
"require": "./dist/register.cjs"
+ },
+ "./mocked": {
+ "types": "./dist/mocked/index.d.ts",
+ "import": "./dist/mocked/index.js",
+ "require": "./dist/mocked/index.cjs"
}
},
"scripts": {
diff --git a/packages/ai-credits-widget/src/AiCreditsWidget.tsx b/packages/ai-credits-widget/src/AiCreditsWidget.tsx
index a8f7ed8c..7ca16b27 100644
--- a/packages/ai-credits-widget/src/AiCreditsWidget.tsx
+++ b/packages/ai-credits-widget/src/AiCreditsWidget.tsx
@@ -35,6 +35,7 @@ import type {
AiCreditsPaySuccessDetail,
AiCreditsPayErrorDetail,
AiCreditsWidgetAdapterFactory,
+ AiCreditsWidgetAdapterOptions,
AiCreditsWidgetAdapterActions,
AiCreditsWidgetAdapterState,
AiCreditsWidgetTab,
@@ -53,6 +54,7 @@ interface AiCreditsInnerProps {
vaultAddress?: string
goodIdAddress?: string
adapterFactory?: AiCreditsWidgetAdapterFactory
+ adapterOptions?: AiCreditsWidgetAdapterOptions
onPaySuccess?: (detail: AiCreditsPaySuccessDetail) => void
onPayError?: (detail: AiCreditsPayErrorDetail) => void
}
@@ -282,6 +284,7 @@ function AiCreditsInner({
vaultAddress,
goodIdAddress,
adapterFactory,
+ adapterOptions,
onPaySuccess,
onPayError,
}: AiCreditsInnerProps) {
@@ -295,6 +298,10 @@ function AiCreditsInner({
goodIdAddress: goodIdAddress as `0x${string}` | undefined,
onPaySuccess,
onPayError,
+ backendClient: adapterOptions?.backendClient,
+ chainClient: adapterOptions?.chainClient,
+ skipVaultPaymentValidation: adapterOptions?.skipVaultPaymentValidation,
+ prepareSettlement: adapterOptions?.prepareSettlement,
})
const activeAdapter = useMemo(
@@ -308,6 +315,7 @@ function AiCreditsInner({
address: state.address,
backendUrl,
environment,
+ backendClient: adapterOptions?.backendClient,
})
const handlePay = useCallback(
@@ -403,6 +411,7 @@ export function AiCreditsWidget({
onPaySuccess,
onPayError,
adapterFactory,
+ adapterOptions,
testId,
}: AiCreditsWidgetProps) {
return (
@@ -423,6 +432,7 @@ export function AiCreditsWidget({
vaultAddress={vaultAddress}
goodIdAddress={goodIdAddress}
adapterFactory={adapterFactory}
+ adapterOptions={adapterOptions}
onPaySuccess={onPaySuccess}
onPayError={onPayError}
/>
diff --git a/packages/ai-credits-widget/src/adapter.ts b/packages/ai-credits-widget/src/adapter.ts
index c6bbc5f4..2c57934a 100644
--- a/packages/ai-credits-widget/src/adapter.ts
+++ b/packages/ai-credits-widget/src/adapter.ts
@@ -15,7 +15,6 @@ import { privateKeyToAccount } from 'viem/accounts'
import { buildBuyerKeyMessage, deriveBuyerPrivateKeyFromSignature } from './buyerKeyDerivation'
import { normalizeChannelId, signRequestClose, signWithdrawPrincipal } from './buyerSignatures'
import {
- MockAiCreditsBackendClient,
totalCreditUsdFromProfile,
buildAccountView,
createBackendClient,
@@ -315,6 +314,10 @@ export interface UseAiCreditsAdapterOptions {
goodIdReturnUrl?: string
onPaySuccess?: (detail: AiCreditsPaySuccessDetail) => void
onPayError?: (detail: AiCreditsPayErrorDetail) => void
+ backendClient?: AiCreditsBackendClient
+ chainClient?: AiCreditsChainClient
+ skipVaultPaymentValidation?: boolean
+ prepareSettlement?: (ref: AccountRef, creditUsd: bigint) => void
}
export function useAiCreditsAdapter({
@@ -328,11 +331,15 @@ export function useAiCreditsAdapter({
goodIdReturnUrl,
onPaySuccess,
onPayError,
+ backendClient: backendClientOverride,
+ chainClient: chainClientOverride,
+ skipVaultPaymentValidation = false,
+ prepareSettlement,
}: UseAiCreditsAdapterOptions): AiCreditsWidgetAdapterResult {
const { address, chainId, isConnected, provider, connect } = useWallet()
const [state, setState] = useState(INITIAL_STATE)
const configurationError =
- environment === 'development' || backendUrl
+ backendClientOverride || backendUrl
? null
: 'AI Credits backend is not configured'
@@ -343,20 +350,21 @@ export function useAiCreditsAdapter({
const celoVault = vaultAddress ?? CELO_GD_ANTSEED_VAULT_FALLBACK
const backendClient = useMemo(
- () => createBackendClient(backendUrl, environment),
- [backendUrl, environment],
+ () => backendClientOverride ?? createBackendClient(backendUrl),
+ [backendClientOverride, backendUrl],
)
const chainClient = useMemo(
() =>
+ chainClientOverride ??
createChainClient({
baseRpcUrl,
celoRpcUrl,
fundingVaultAddress,
celoVaultAddress: celoVault,
celoGoodIdAddress: goodIdAddress ?? CELO_GOODID_ADDRESS,
- }, environment === 'development' ? 'development' : 'production'),
- [environment, baseRpcUrl, celoRpcUrl, fundingVaultAddress, celoVault, goodIdAddress],
+ }),
+ [chainClientOverride, baseRpcUrl, celoRpcUrl, fundingVaultAddress, celoVault, goodIdAddress],
)
useEffect(() => {
@@ -428,7 +436,7 @@ export function useAiCreditsAdapter({
.catch(() => null)
const minimumsPromise =
- backendClient instanceof MockAiCreditsBackendClient
+ skipVaultPaymentValidation
? Promise.resolve({
minDepositUsd: '1.00',
minStreamUsd: '1.00',
@@ -738,7 +746,7 @@ export function useAiCreditsAdapter({
throw new Error('Could not build quote — check chain connectivity')
}
- if (!(backendClient instanceof MockAiCreditsBackendClient)) {
+ if (!skipVaultPaymentValidation) {
try {
const publicClient = createPublicClient({ chain: CELO_CHAIN, transport: http() })
await validateVaultPaymentAmounts({
@@ -790,12 +798,12 @@ export function useAiCreditsAdapter({
buyer: currentState.buyerPubKey,
}
- if (backendClient instanceof MockAiCreditsBackendClient) {
+ if (prepareSettlement) {
const creditUsdMicro = quoteTotalUsdMicro(quote, gdUsdPerToken, currentState.isGoodIdVerified, {
depositBonusPercent: currentState.depositBonusPercent,
streamBonusPercent: currentState.streamBonusPercent,
})
- backendClient.prepareSettlement(accountRef, creditUsdMicro)
+ prepareSettlement(accountRef, creditUsdMicro)
}
const { txHashes } = await executeCeloPayment({
@@ -864,7 +872,16 @@ export function useAiCreditsAdapter({
throw new Error(message)
}
},
- [state, backendClient, chainClient, celoVault, onPaySuccess, onPayError],
+ [
+ state,
+ backendClient,
+ chainClient,
+ celoVault,
+ onPaySuccess,
+ onPayError,
+ prepareSettlement,
+ skipVaultPaymentValidation,
+ ],
)
const handleRefresh = useCallback(
diff --git a/packages/ai-credits-widget/src/backendClient.ts b/packages/ai-credits-widget/src/backendClient.ts
index 7db5d8ca..d5f0bd54 100644
--- a/packages/ai-credits-widget/src/backendClient.ts
+++ b/packages/ai-credits-widget/src/backendClient.ts
@@ -13,7 +13,7 @@ import type {
UserCreditProfile,
} from './backendTypes'
import type { BuyerOperatorStatus } from './operatorConsent'
-import { markMockOperatorConsent, type AiCreditsChainClient } from './chainClient'
+import type { AiCreditsChainClient } from './chainClient'
import {
flowRateWeiToMonthlyG,
weiToG,
@@ -126,111 +126,6 @@ function clampHistoryLimit(limit: number | undefined): number {
return Math.min(Math.max(1, Math.floor(limit)), MAX_HISTORY_LIMIT)
}
-function paginateGdCredits(
- entries: GdCreditEntry[],
- options: CreditHistoryQuery = {},
-): CreditHistoryResponse {
- let result = [...entries].sort((a, b) => {
- const byCreated = b.createdAt.localeCompare(a.createdAt)
- if (byCreated !== 0) return byCreated
- return b.id.localeCompare(a.id)
- })
- if (options.source) {
- result = result.filter((entry) => entry.source === options.source)
- }
- if (options.fundingStatus) {
- result = result.filter((entry) => entry.fundingStatus === options.fundingStatus)
- }
- if (options.from) {
- const fromMs = Date.parse(options.from)
- result = result.filter((entry) => Date.parse(entry.createdAt) >= fromMs)
- }
- if (options.to) {
- const toMs = Date.parse(options.to)
- result = result.filter((entry) => Date.parse(entry.createdAt) <= toMs)
- }
- const limit = clampHistoryLimit(options.limit)
- const offset = Math.max(0, Math.floor(options.offset ?? 0))
- const total = result.length
- const items = result.slice(offset, offset + limit)
- return {
- account: '',
- items,
- total,
- limit,
- offset,
- hasMore: offset + limit < total,
- }
-}
-
-function createDemoHistory(account: string): GdCreditEntry[] {
- const normalized = normalizeAddress(account)
- return [
- {
- id: 'demo-deposit-1',
- account: normalized,
- rootAccount: normalized,
- source: 'deposit',
- gdAmountWei: '440000000000000000000',
- totalCreditUsd: '6600',
- principalUsd: '6000',
- bonusUsd: '600',
- fundingStatus: 'funded',
- txHash: '0xdemo01',
- logIndex: 0,
- createdAt: '2026-06-25T14:30:00.000Z',
- streamUpdateMonth: '2026-06',
- buyerAddress: normalized,
- },
- {
- id: 'demo-stream-update-1',
- account: normalized,
- rootAccount: normalized,
- source: 'streamUpdate',
- gdAmountWei: '10000000000000000000',
- totalCreditUsd: '0',
- principalUsd: '0',
- bonusUsd: '0',
- fundingStatus: 'funded',
- txHash: '0xdemo02',
- logIndex: 0,
- createdAt: '2026-06-24T12:00:00.000Z',
- streamUpdateMonth: '2026-06',
- buyerAddress: normalized,
- },
- {
- id: 'demo-stream-request-1',
- account: normalized,
- rootAccount: normalized,
- source: 'streamRequest',
- gdAmountWei: '8000000000000000000',
- totalCreditUsd: '1200',
- principalUsd: '1000',
- bonusUsd: '200',
- fundingStatus: 'pending',
- txHash: '0xdemo03',
- logIndex: 0,
- createdAt: '2026-06-23T09:15:00.000Z',
- streamUpdateMonth: '2026-06',
- buyerAddress: normalized,
- },
- {
- id: 'demo-stream-cron-1',
- account: normalized,
- rootAccount: normalized,
- source: 'streamCron',
- gdAmountWei: '5000000000000000000',
- totalCreditUsd: '900',
- principalUsd: '750',
- bonusUsd: '150',
- fundingStatus: 'funded',
- createdAt: '2026-06-22T08:00:00.000Z',
- streamUpdateMonth: '2026-06',
- buyerAddress: normalized,
- },
- ]
-}
-
export function resolveBuyerAddress(entries: GdCreditEntry[]): string | null {
for (const entry of entries) {
if (entry.buyerAddress && isAddress(entry.buyerAddress)) {
@@ -321,7 +216,6 @@ export interface AiCreditsBackendClient {
): Promise
}
-const MOCK_DELAY_MS = 600
const BPS_PER_PERCENT = 100
export const DEFAULT_DISCOUNT_CONFIG: DiscountConfig = {
@@ -351,185 +245,6 @@ function discountConfigFromConfigValues(
}
}
-export class MockAiCreditsBackendClient implements AiCreditsBackendClient {
- private activeRef: AccountRef | null = null
- private lastCreditUsd = 0n
-
- private readonly accountStates = new Map<
- string,
- {
- principalUsd: bigint
- bonusUsd: bigint
- transactions: GdCreditEntry[]
- rootAccount: string
- }
- >()
-
- private getState(payer: string) {
- const key = normalizeAddress(payer)
- if (!this.accountStates.has(key)) {
- this.accountStates.set(key, {
- principalUsd: 0n,
- bonusUsd: 0n,
- transactions: createDemoHistory(key),
- rootAccount: key,
- })
- }
- return this.accountStates.get(key)!
- }
-
- async getDiscountConfig(): Promise {
- await sleep(MOCK_DELAY_MS)
- return { ...DEFAULT_DISCOUNT_CONFIG }
- }
-
- private buildProfile(payer: string): UserCreditProfile {
- const state = this.getState(payer)
- const now = new Date().toISOString()
- const outstanding = state.transactions
- .filter((entry) => entry.fundingStatus === 'pending' || entry.fundingStatus === 'failed')
- .reduce((sum, entry) => sum + BigInt(entry.totalCreditUsd), 0n)
- return {
- account: normalizeAddress(payer),
- rootAccount: state.rootAccount,
- createdAt: now,
- updatedAt: now,
- totalGdDepositedWei: '0',
- totalPrincipalUsd: state.principalUsd.toString(),
- totalBonusUsd: state.bonusUsd.toString(),
- totalGDStreamedWei: '0',
- totalOutstandingFundingUsd: outstanding.toString(),
- streamFlowRateWeiPerSecond: '0',
- }
- }
-
- async getAccountCredit(payer: string): Promise {
- await sleep(MOCK_DELAY_MS)
- const profile = this.buildProfile(payer)
- return {
- account: profile.account,
- profile,
- }
- }
-
- async getCreditHistory(
- payer: string,
- options: CreditHistoryQuery = {},
- ): Promise {
- await sleep(MOCK_DELAY_MS)
- const history = paginateGdCredits(this.getState(payer).transactions, options)
- return {
- ...history,
- account: normalizeAddress(payer),
- }
- }
-
- async getOutstanding(payer: string): Promise<{ outstandingFundingUsd: string; count: number }> {
- await sleep(MOCK_DELAY_MS)
- const state = this.getState(payer)
- const pending = state.transactions.filter(
- (entry) => entry.fundingStatus === 'pending' || entry.fundingStatus === 'failed',
- )
- const outstanding = pending.reduce(
- (sum, entry) => sum + BigInt(entry.totalCreditUsd || '0'),
- 0n,
- )
- return { outstandingFundingUsd: outstanding.toString(), count: pending.length }
- }
-
- async getTransactions(
- payer: string,
- options: { status?: 'pending' | 'funded' | 'failed'; limit?: number; cursor?: string } = {},
- ): Promise {
- const history = await this.getCreditHistory(payer, {
- fundingStatus: options.status,
- limit: options.limit,
- offset: options.cursor ? Number(options.cursor) || 0 : 0,
- })
- return { account: history.account, transactions: history.items }
- }
-
- async getUsageLog(payer: string): Promise {
- const history = await this.getCreditHistory(payer, { limit: DEFAULT_HISTORY_LIMIT, offset: 0 })
- return history.items
- }
-
- prepareSettlement(ref: AccountRef, creditUsd: bigint): void {
- this.activeRef = { payer: normalizeAddress(ref.payer), buyer: normalizeAddress(ref.buyer) }
- this.lastCreditUsd = creditUsd
- }
-
- async notifyPayment(txHash: string): Promise {
- await sleep(MOCK_DELAY_MS)
- const ref = this.activeRef
- if (!ref) return { txHash, events: [] }
-
- const totalUsd = this.lastCreditUsd
- const now = new Date().toISOString()
- const entry: GdCreditEntry = {
- id: `${txHash}:0`,
- account: ref.payer,
- rootAccount: ref.payer,
- source: 'deposit',
- gdAmountWei: '0',
- totalCreditUsd: totalUsd.toString(),
- principalUsd: totalUsd.toString(),
- bonusUsd: '0',
- fundingStatus: 'pending',
- txHash,
- logIndex: 0,
- createdAt: now,
- streamUpdateMonth: now.slice(0, 7),
- buyerAddress: ref.buyer,
- }
-
- const state = this.getState(ref.payer)
- if (!state.transactions.find((item) => item.id === entry.id)) {
- state.transactions.unshift(entry)
- }
- return { txHash, events: [entry] }
- }
-
- async waitForSettlement(ref: AccountRef): Promise {
- await sleep(MOCK_DELAY_MS)
- const state = this.getState(ref.payer)
- for (const entry of state.transactions) {
- if (entry.fundingStatus !== 'pending') continue
- entry.fundingStatus = 'funded'
- state.principalUsd += BigInt(entry.principalUsd)
- state.bonusUsd += BigInt(entry.bonusUsd)
- }
- return { totalCreditUsd: totalCreditUsdFromProfile(this.buildProfile(ref.payer)) }
- }
-
- async closeChannel(channelId: string): Promise {
- await sleep(MOCK_DELAY_MS)
- return { channelId, action: 'close', bridge: { enabled: true, txHash: '0xmock' } }
- }
-
- async withdrawCredits(
- _buyer: string,
- body: WithdrawPrincipalRequest,
- ): Promise {
- await sleep(MOCK_DELAY_MS)
- return {
- account: _buyer,
- amountUsd: body.amount,
- bridge: { enabled: true, txHash: '0xmock' },
- }
- }
-
- async submitOperatorConsent(buyer: string): Promise {
- await sleep(MOCK_DELAY_MS)
- const normalizedBuyer = normalizeAddress(buyer)
- markMockOperatorConsent(normalizedBuyer)
- return {
- buyer: normalizedBuyer,
- bridge: { enabled: true, txHash: '0xmock' },
- }
- }
-}
-
export class ProductionAiCreditsBackendClient implements AiCreditsBackendClient {
private readonly backendUrl: string
@@ -749,11 +464,7 @@ export class UnavailableAiCreditsBackendClient implements AiCreditsBackendClient
export function createBackendClient(
backendUrl: string | undefined,
- environment: 'production' | 'staging' | 'development' = 'production',
): AiCreditsBackendClient {
- if (!backendUrl && environment === 'development') {
- return new MockAiCreditsBackendClient()
- }
if (!backendUrl) {
return new UnavailableAiCreditsBackendClient()
}
diff --git a/packages/ai-credits-widget/src/chainClient.ts b/packages/ai-credits-widget/src/chainClient.ts
index 310a8aa3..8d639777 100644
--- a/packages/ai-credits-widget/src/chainClient.ts
+++ b/packages/ai-credits-widget/src/chainClient.ts
@@ -61,12 +61,6 @@ function normalizeAddress(address: string): string {
return address.toLowerCase()
}
-const mockOperatorAcceptedBuyers = new Set()
-
-export function markMockOperatorConsent(buyer: string): void {
- mockOperatorAcceptedBuyers.add(normalizeAddress(buyer))
-}
-
export type AiCreditsChainClientOptions = {
baseRpcUrl?: string
fundingVaultAddress?: Address
@@ -241,85 +235,9 @@ export class ProductionAiCreditsChainClient implements AiCreditsChainClient {
}
}
-export class MockAiCreditsChainClient implements AiCreditsChainClient {
- private operatorAccepted: boolean
- private readonly gdUsdPerToken: number
- private readonly goodIdVerified: boolean
-
- constructor(
- options: { operatorAccepted?: boolean; gdUsdPerToken?: number; goodIdVerified?: boolean } = {},
- ) {
- this.operatorAccepted = options.operatorAccepted ?? false
- this.gdUsdPerToken = options.gdUsdPerToken ?? 0.0015
- this.goodIdVerified = options.goodIdVerified ?? false
- }
-
- async isGoodIdVerified(): Promise {
- return this.goodIdVerified
- }
-
- async fetchGdUsdPerToken(): Promise {
- return this.gdUsdPerToken
- }
-
- async buildQuote(depositG: string, streamG: string): Promise {
- return buildQuoteAmounts(depositG, streamG)
- }
-
- async getBuyerOperatorStatus(ref: AccountRef): Promise {
- const payer = normalizeAddress(ref.payer)
- const buyer = normalizeAddress(ref.buyer)
- const operatorAddress = '0x0000000000000000000000000000000000000004'
- const operatorAccepted = this.operatorAccepted || mockOperatorAcceptedBuyers.has(buyer)
- return {
- enabled: true,
- account: payer,
- buyerAddress: buyer,
- operatorAddress,
- currentOperator: operatorAccepted ? operatorAddress : '0x0000000000000000000000000000000000000000',
- operatorAccepted,
- consentNonce: '0',
- }
- }
-
- async buildOperatorConsentPayload(
- ref: AccountRef,
- operatorStatus?: BuyerOperatorStatus,
- ): Promise {
- const status = operatorStatus ?? (await this.getBuyerOperatorStatus(ref))
- if (!status.enabled || !status.operatorAddress) {
- return {
- enabled: false,
- account: normalizeAddress(ref.payer),
- buyerAddress: normalizeAddress(ref.buyer),
- }
- }
- return {
- enabled: true,
- account: normalizeAddress(ref.payer),
- buyerAddress: normalizeAddress(ref.buyer),
- typedData: buildSetOperatorPayload(
- BASE_CHAIN_ID,
- ANTSEED_DEPOSITS_BASE_ADDRESS,
- status.operatorAddress,
- BigInt(status.consentNonce),
- { name: 'AntseedDeposits', version: '1' },
- ),
- }
- }
-
- async getWithdrawableUsd(): Promise {
- return '0'
- }
-}
-
export function createChainClient(
options: AiCreditsChainClientOptions = {},
- environment: 'development' | 'production' = 'production',
): AiCreditsChainClient {
- if (environment === 'development') {
- return new MockAiCreditsChainClient()
- }
return new ProductionAiCreditsChainClient({
...options,
celoGoodIdAddress: options.celoGoodIdAddress ?? CELO_GOODID_ADDRESS,
diff --git a/packages/ai-credits-widget/src/index.ts b/packages/ai-credits-widget/src/index.ts
index ee429068..451978c7 100644
--- a/packages/ai-credits-widget/src/index.ts
+++ b/packages/ai-credits-widget/src/index.ts
@@ -26,7 +26,6 @@ export type {
GdCreditEntry,
} from './backendClient'
export {
- MockAiCreditsBackendClient,
ProductionAiCreditsBackendClient,
UnavailableAiCreditsBackendClient,
createBackendClient,
diff --git a/packages/ai-credits-widget/src/mocked/AiCreditsMockWidget.tsx b/packages/ai-credits-widget/src/mocked/AiCreditsMockWidget.tsx
new file mode 100644
index 00000000..33f4f374
--- /dev/null
+++ b/packages/ai-credits-widget/src/mocked/AiCreditsMockWidget.tsx
@@ -0,0 +1,22 @@
+import { useMemo } from 'react'
+import { AiCreditsWidget, type AiCreditsWidgetProps } from '..'
+import { MockAiCreditsBackendClient } from './backendClient'
+import { MockAiCreditsChainClient } from './chainClient'
+
+export function MockAiCreditsWidget(props: Omit) {
+ const backendClient = useMemo(() => new MockAiCreditsBackendClient(), [])
+ const chainClient = useMemo(() => new MockAiCreditsChainClient(), [])
+
+ return (
+ backendClient.prepareSettlement(ref, creditUsd),
+ }}
+ />
+ )
+}
diff --git a/packages/ai-credits-widget/src/mocked/backendClient.ts b/packages/ai-credits-widget/src/mocked/backendClient.ts
new file mode 100644
index 00000000..7be81b93
--- /dev/null
+++ b/packages/ai-credits-widget/src/mocked/backendClient.ts
@@ -0,0 +1,212 @@
+import type {
+ AccountCreditResponse,
+ AccountRef,
+ CeloEventsRecordResponse,
+ ChannelOperationResponse,
+ CreditHistoryQuery,
+ CreditHistoryResponse,
+ DiscountConfig,
+ GdCreditEntry,
+ OperatorConsentResponse,
+ SettlementResult,
+ TransactionsResponse,
+ UserCreditProfile,
+ WithdrawPrincipalRequest,
+ WithdrawPrincipalResponse,
+} from '../backendClient'
+import { DEFAULT_DISCOUNT_CONFIG, totalCreditUsdFromProfile } from '../backendClient'
+import type { AiCreditsBackendClient } from '../backendClient'
+import { markMockOperatorConsent } from './chainClient'
+
+const MOCK_DELAY_MS = 600
+const DEFAULT_HISTORY_LIMIT = 20
+const MAX_HISTORY_LIMIT = 100
+
+function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+function normalizeAddress(address: string): string {
+ return address.toLowerCase()
+}
+
+function paginateGdCredits(
+ entries: GdCreditEntry[],
+ options: CreditHistoryQuery = {},
+): CreditHistoryResponse {
+ let result = [...entries].sort((a, b) => {
+ const byCreated = b.createdAt.localeCompare(a.createdAt)
+ return byCreated !== 0 ? byCreated : b.id.localeCompare(a.id)
+ })
+ if (options.source) result = result.filter((entry) => entry.source === options.source)
+ if (options.fundingStatus) result = result.filter((entry) => entry.fundingStatus === options.fundingStatus)
+ if (options.from) {
+ const fromMs = Date.parse(options.from)
+ result = result.filter((entry) => Date.parse(entry.createdAt) >= fromMs)
+ }
+ if (options.to) {
+ const toMs = Date.parse(options.to)
+ result = result.filter((entry) => Date.parse(entry.createdAt) <= toMs)
+ }
+ const limit = options.limit === undefined
+ ? DEFAULT_HISTORY_LIMIT
+ : Math.min(Math.max(1, Math.floor(options.limit)), MAX_HISTORY_LIMIT)
+ const offset = Math.max(0, Math.floor(options.offset ?? 0))
+ return {
+ account: '',
+ items: result.slice(offset, offset + limit),
+ total: result.length,
+ limit,
+ offset,
+ hasMore: offset + limit < result.length,
+ }
+}
+
+function createDemoHistory(account: string): GdCreditEntry[] {
+ const normalized = normalizeAddress(account)
+ return [
+ {
+ id: 'demo-deposit-1', account: normalized, rootAccount: normalized, source: 'deposit',
+ gdAmountWei: '440000000000000000000', totalCreditUsd: '6600', principalUsd: '6000',
+ bonusUsd: '600', fundingStatus: 'funded', txHash: '0xdemo01', logIndex: 0,
+ createdAt: '2026-06-25T14:30:00.000Z', streamUpdateMonth: '2026-06', buyerAddress: normalized,
+ },
+ {
+ id: 'demo-stream-update-1', account: normalized, rootAccount: normalized, source: 'streamUpdate',
+ gdAmountWei: '10000000000000000000', totalCreditUsd: '0', principalUsd: '0',
+ bonusUsd: '0', fundingStatus: 'funded', txHash: '0xdemo02', logIndex: 0,
+ createdAt: '2026-06-24T12:00:00.000Z', streamUpdateMonth: '2026-06', buyerAddress: normalized,
+ },
+ {
+ id: 'demo-stream-request-1', account: normalized, rootAccount: normalized, source: 'streamRequest',
+ gdAmountWei: '8000000000000000000', totalCreditUsd: '1200', principalUsd: '1000',
+ bonusUsd: '200', fundingStatus: 'pending', txHash: '0xdemo03', logIndex: 0,
+ createdAt: '2026-06-23T09:15:00.000Z', streamUpdateMonth: '2026-06', buyerAddress: normalized,
+ },
+ {
+ id: 'demo-stream-cron-1', account: normalized, rootAccount: normalized, source: 'streamCron',
+ gdAmountWei: '5000000000000000000', totalCreditUsd: '900', principalUsd: '750',
+ bonusUsd: '150', fundingStatus: 'funded', createdAt: '2026-06-22T08:00:00.000Z',
+ streamUpdateMonth: '2026-06', buyerAddress: normalized,
+ },
+ ]
+}
+
+export class MockAiCreditsBackendClient implements AiCreditsBackendClient {
+ private activeRef: AccountRef | null = null
+ private lastCreditUsd = 0n
+ private readonly accountStates = new Map()
+
+ private getState(payer: string) {
+ const key = normalizeAddress(payer)
+ if (!this.accountStates.has(key)) {
+ this.accountStates.set(key, {
+ principalUsd: 0n, bonusUsd: 0n, transactions: createDemoHistory(key), rootAccount: key,
+ })
+ }
+ return this.accountStates.get(key)!
+ }
+
+ async getDiscountConfig(): Promise {
+ await sleep(MOCK_DELAY_MS)
+ return { ...DEFAULT_DISCOUNT_CONFIG }
+ }
+
+ private buildProfile(payer: string): UserCreditProfile {
+ const state = this.getState(payer)
+ const now = new Date().toISOString()
+ const outstanding = state.transactions
+ .filter((entry) => entry.fundingStatus === 'pending' || entry.fundingStatus === 'failed')
+ .reduce((sum, entry) => sum + BigInt(entry.totalCreditUsd), 0n)
+ return {
+ account: normalizeAddress(payer), rootAccount: state.rootAccount, createdAt: now, updatedAt: now,
+ totalGdDepositedWei: '0', totalPrincipalUsd: state.principalUsd.toString(),
+ totalBonusUsd: state.bonusUsd.toString(), totalGDStreamedWei: '0',
+ totalOutstandingFundingUsd: outstanding.toString(), streamFlowRateWeiPerSecond: '0',
+ }
+ }
+
+ async getAccountCredit(payer: string): Promise {
+ await sleep(MOCK_DELAY_MS)
+ const profile = this.buildProfile(payer)
+ return { account: profile.account, profile }
+ }
+
+ async getCreditHistory(payer: string, options: CreditHistoryQuery = {}): Promise {
+ await sleep(MOCK_DELAY_MS)
+ return { ...paginateGdCredits(this.getState(payer).transactions, options), account: normalizeAddress(payer) }
+ }
+
+ async getOutstanding(payer: string): Promise<{ outstandingFundingUsd: string; count: number }> {
+ await sleep(MOCK_DELAY_MS)
+ const pending = this.getState(payer).transactions.filter(
+ (entry) => entry.fundingStatus === 'pending' || entry.fundingStatus === 'failed',
+ )
+ return {
+ outstandingFundingUsd: pending.reduce((sum, entry) => sum + BigInt(entry.totalCreditUsd || '0'), 0n).toString(),
+ count: pending.length,
+ }
+ }
+
+ async getTransactions(payer: string, options: { status?: 'pending' | 'funded' | 'failed'; limit?: number; cursor?: string } = {}): Promise {
+ const history = await this.getCreditHistory(payer, {
+ fundingStatus: options.status, limit: options.limit, offset: options.cursor ? Number(options.cursor) || 0 : 0,
+ })
+ return { account: history.account, transactions: history.items }
+ }
+
+ prepareSettlement(ref: AccountRef, creditUsd: bigint): void {
+ this.activeRef = { payer: normalizeAddress(ref.payer), buyer: normalizeAddress(ref.buyer) }
+ this.lastCreditUsd = creditUsd
+ }
+
+ async notifyPayment(txHash: string): Promise {
+ await sleep(MOCK_DELAY_MS)
+ const ref = this.activeRef
+ if (!ref) return { txHash, events: [] }
+ const now = new Date().toISOString()
+ const entry: GdCreditEntry = {
+ id: `${txHash}:0`, account: ref.payer, rootAccount: ref.payer, source: 'deposit',
+ gdAmountWei: '0', totalCreditUsd: this.lastCreditUsd.toString(), principalUsd: this.lastCreditUsd.toString(),
+ bonusUsd: '0', fundingStatus: 'pending', txHash, logIndex: 0, createdAt: now,
+ streamUpdateMonth: now.slice(0, 7), buyerAddress: ref.buyer,
+ }
+ const state = this.getState(ref.payer)
+ if (!state.transactions.find((item) => item.id === entry.id)) state.transactions.unshift(entry)
+ return { txHash, events: [entry] }
+ }
+
+ async waitForSettlement(ref: AccountRef): Promise {
+ await sleep(MOCK_DELAY_MS)
+ const state = this.getState(ref.payer)
+ for (const entry of state.transactions) {
+ if (entry.fundingStatus !== 'pending') continue
+ entry.fundingStatus = 'funded'
+ state.principalUsd += BigInt(entry.principalUsd)
+ state.bonusUsd += BigInt(entry.bonusUsd)
+ }
+ return { totalCreditUsd: totalCreditUsdFromProfile(this.buildProfile(ref.payer)) }
+ }
+
+ async closeChannel(channelId: string): Promise {
+ await sleep(MOCK_DELAY_MS)
+ return { channelId, action: 'close', bridge: { enabled: true, txHash: '0xmock' } }
+ }
+
+ async withdrawCredits(buyer: string, body: WithdrawPrincipalRequest): Promise {
+ await sleep(MOCK_DELAY_MS)
+ return { account: buyer, amountUsd: body.amount, bridge: { enabled: true, txHash: '0xmock' } }
+ }
+
+ async submitOperatorConsent(buyer: string): Promise {
+ await sleep(MOCK_DELAY_MS)
+ const normalizedBuyer = normalizeAddress(buyer)
+ markMockOperatorConsent(normalizedBuyer)
+ return { buyer: normalizedBuyer, bridge: { enabled: true, txHash: '0xmock' } }
+ }
+}
diff --git a/packages/ai-credits-widget/src/mocked/chainClient.ts b/packages/ai-credits-widget/src/mocked/chainClient.ts
new file mode 100644
index 00000000..16d14361
--- /dev/null
+++ b/packages/ai-credits-widget/src/mocked/chainClient.ts
@@ -0,0 +1,88 @@
+import type { AccountRef } from '../backendTypes'
+import { BASE_CHAIN_ID, type AiCreditsChainClient } from '../chainClient'
+import { ANTSEED_DEPOSITS_BASE_ADDRESS, buildSetOperatorPayload } from '../operatorConsent'
+import { buildQuoteAmounts } from '../quoteMath'
+import type { BuyerOperatorStatus, OperatorConsentPayloadResponse } from '../operatorConsent'
+import type { AiCreditsQuote } from '../widgetRuntimeContract'
+
+const mockOperatorAcceptedBuyers = new Set()
+
+function normalizeAddress(address: string): string {
+ return address.toLowerCase()
+}
+
+export function markMockOperatorConsent(buyer: string): void {
+ mockOperatorAcceptedBuyers.add(normalizeAddress(buyer))
+}
+
+export class MockAiCreditsChainClient implements AiCreditsChainClient {
+ private operatorAccepted: boolean
+ private readonly gdUsdPerToken: number
+ private readonly goodIdVerified: boolean
+
+ constructor(
+ options: { operatorAccepted?: boolean; gdUsdPerToken?: number; goodIdVerified?: boolean } = {},
+ ) {
+ this.operatorAccepted = options.operatorAccepted ?? false
+ this.gdUsdPerToken = options.gdUsdPerToken ?? 0.0015
+ this.goodIdVerified = options.goodIdVerified ?? false
+ }
+
+ async isGoodIdVerified(): Promise {
+ return this.goodIdVerified
+ }
+
+ async fetchGdUsdPerToken(): Promise {
+ return this.gdUsdPerToken
+ }
+
+ async buildQuote(depositG: string, streamG: string): Promise {
+ return buildQuoteAmounts(depositG, streamG)
+ }
+
+ async getBuyerOperatorStatus(ref: AccountRef): Promise {
+ const payer = normalizeAddress(ref.payer)
+ const buyer = normalizeAddress(ref.buyer)
+ const operatorAddress = '0x0000000000000000000000000000000000000004'
+ const operatorAccepted = this.operatorAccepted || mockOperatorAcceptedBuyers.has(buyer)
+ return {
+ enabled: true,
+ account: payer,
+ buyerAddress: buyer,
+ operatorAddress,
+ currentOperator: operatorAccepted ? operatorAddress : '0x0000000000000000000000000000000000000000',
+ operatorAccepted,
+ consentNonce: '0',
+ }
+ }
+
+ async buildOperatorConsentPayload(
+ ref: AccountRef,
+ operatorStatus?: BuyerOperatorStatus,
+ ): Promise {
+ const status = operatorStatus ?? (await this.getBuyerOperatorStatus(ref))
+ if (!status.enabled || !status.operatorAddress) {
+ return {
+ enabled: false,
+ account: normalizeAddress(ref.payer),
+ buyerAddress: normalizeAddress(ref.buyer),
+ }
+ }
+ return {
+ enabled: true,
+ account: normalizeAddress(ref.payer),
+ buyerAddress: normalizeAddress(ref.buyer),
+ typedData: buildSetOperatorPayload(
+ BASE_CHAIN_ID,
+ ANTSEED_DEPOSITS_BASE_ADDRESS,
+ status.operatorAddress,
+ BigInt(status.consentNonce),
+ { name: 'AntseedDeposits', version: '1' },
+ ),
+ }
+ }
+
+ async getWithdrawableUsd(): Promise {
+ return '0'
+ }
+}
diff --git a/packages/ai-credits-widget/src/mocked/index.ts b/packages/ai-credits-widget/src/mocked/index.ts
new file mode 100644
index 00000000..19d45fc8
--- /dev/null
+++ b/packages/ai-credits-widget/src/mocked/index.ts
@@ -0,0 +1,3 @@
+export { MockAiCreditsWidget } from './AiCreditsMockWidget'
+export { MockAiCreditsBackendClient } from './backendClient'
+export { MockAiCreditsChainClient } from './chainClient'
diff --git a/packages/ai-credits-widget/src/useAiCreditsHistory.ts b/packages/ai-credits-widget/src/useAiCreditsHistory.ts
index 49bba754..f63dfc28 100644
--- a/packages/ai-credits-widget/src/useAiCreditsHistory.ts
+++ b/packages/ai-credits-widget/src/useAiCreditsHistory.ts
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import type { GdCreditEntry } from './backendTypes'
import { createBackendClient } from './backendClient'
+import type { AiCreditsBackendClient } from './backendClient'
import type { AiCreditsWidgetEnvironment } from './widgetRuntimeContract'
export const HISTORY_PAGE_SIZE = 10
@@ -86,8 +87,9 @@ export function useAiCreditsHistory(options: {
address: string | null
backendUrl?: string
environment?: AiCreditsWidgetEnvironment
+ backendClient?: AiCreditsBackendClient
}): UseAiCreditsHistoryResult {
- const { address, backendUrl, environment = 'production' } = options
+ const { address, backendUrl, environment = 'production', backendClient } = options
const defaultRange = useMemo(() => getLast90DaysRange(), [])
const [selectedSources, setSelectedSources] = useState(createDefaultSelectedSources)
@@ -132,7 +134,7 @@ export function useAiCreditsHistory(options: {
else setLoading(true)
setError(null)
- const client = createBackendClient(backendUrl, environment)
+ const client = backendClient ?? createBackendClient(backendUrl)
const apiSource = activeSources.length === 1 ? activeSources[0] : undefined
const fundingStatus = statusFilter === 'all' ? undefined : statusFilter
@@ -162,7 +164,17 @@ export function useAiCreditsHistory(options: {
setLoadingMore(false)
}
},
- [address, backendUrl, environment, activeSources, statusFilter, fromDate, toDate, selectedSources],
+ [
+ address,
+ backendUrl,
+ environment,
+ backendClient,
+ activeSources,
+ statusFilter,
+ fromDate,
+ toDate,
+ selectedSources,
+ ],
)
useEffect(() => {
diff --git a/packages/ai-credits-widget/src/widgetRuntimeContract.ts b/packages/ai-credits-widget/src/widgetRuntimeContract.ts
index a3aeebe8..65a06de8 100644
--- a/packages/ai-credits-widget/src/widgetRuntimeContract.ts
+++ b/packages/ai-credits-widget/src/widgetRuntimeContract.ts
@@ -1,5 +1,8 @@
import type { Address } from 'viem'
import type { GoodWidgetConfig, GoodWidgetThemeOverrides } from '@goodwidget/ui'
+import type { AccountRef } from './backendTypes'
+import type { AiCreditsBackendClient } from './backendClient'
+import type { AiCreditsChainClient } from './chainClient'
export type AiCreditsWidgetEnvironment = 'production' | 'staging' | 'development'
@@ -76,6 +79,13 @@ export type AiCreditsWidgetAdapterFactory = (
input: AiCreditsWidgetAdapterFactoryInput,
) => AiCreditsWidgetAdapterResult
+export interface AiCreditsWidgetAdapterOptions {
+ backendClient?: AiCreditsBackendClient
+ chainClient?: AiCreditsChainClient
+ skipVaultPaymentValidation?: boolean
+ prepareSettlement?: (ref: AccountRef, creditUsd: bigint) => void
+}
+
export interface AiCreditsPaySuccessDetail {
address: string
chainId: number
@@ -106,5 +116,6 @@ export interface AiCreditsWidgetProps {
onPaySuccess?: (detail: AiCreditsPaySuccessDetail) => void
onPayError?: (detail: AiCreditsPayErrorDetail) => void
adapterFactory?: AiCreditsWidgetAdapterFactory
+ adapterOptions?: AiCreditsWidgetAdapterOptions
testId?: string
}
diff --git a/packages/ai-credits-widget/tsup.config.ts b/packages/ai-credits-widget/tsup.config.ts
index 95c2de0d..ec345df0 100644
--- a/packages/ai-credits-widget/tsup.config.ts
+++ b/packages/ai-credits-widget/tsup.config.ts
@@ -5,6 +5,7 @@ export default defineConfig({
index: 'src/index.ts',
element: 'src/element.ts',
register: 'src/register.ts',
+ 'mocked/index': 'src/mocked/index.ts',
},
format: ['esm', 'cjs'],
dts: true,