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,