| name | adobe-add-on-credits-flow |
| description | Audit, implement, or repair Adobe Express add-on credits behavior. Use for 积分模块, credits, free_cnt, paid credits, zero-credit preservation, balance drift, successful-use deduction, payment-return refresh, page-reload balance persistence, purchase-delta balance updates, PayLink polling, and stale remote user/status problems. |
| argument-hint | [project-path] [reference-path] |
Adobe Add-on Credits Flow
When To Use
Use this skill when an Adobe Express add-on has any credits or billing-status issue:
- The user says
积分模块, 积分乱跳, 用完后变为 0, 支付后不到账, 刷新后积分恢复, or similar.
- Credits reset after panel reload or browser refresh, for example a user spends the last credit but the footer returns to
Use 1 of 1.
- Credits need to be audited, ported from a working reference project, or implemented in a migrated Adobe add-on.
- Payment return, pricing links, paid credits,
free_cnt, monthly usage, or no-credit gating are involved.
If the task also requires auth-loop repair, local server lifecycle, version bump, and dist.zip packaging, pair this with the broader Adobe auth/refresh/packaging workflow. This skill owns the credits contract itself.
Related Adobe Skills
Keep this skill independent, but connect it to the rest of the Adobe repair stack when the task crosses boundaries:
- Pair with
subscription-credits-guardrails when credits interact with subscription state, monthly quota, subscribed footer totals, or monthly-then-purchased deduction.
- Pair with
adobe-add-on-login-loop-fix when payment return, focus, visibility, or status refresh causes repeated canvaid-login, canva-passport, or /status calls.
- Pair with
adobe-add-on-request-params when credit/status/generation requests also need uid, platform_uid, platform=adobe_addon, app_channel, or Accept-Language repair.
- Pair with
migrate-adobe-app-to-template-v2 when the app is being moved to template-v2 and credits are one part of a larger Adobe migration.
- Pair with
adobe-addons-collaboration before editing YiChongmimi repos, and adobe-addons-push when the repaired project needs commit, PR, package verification, or management sync.
Do not move or drop the optimization points below. Other Adobe skills may reference this skill for the normalized credits lifecycle, including remote status parsing, zero preservation, cache behavior, payment-return baseline handling, and successful-use deduction.
Specialization Boundary
This skill is the generic credits lifecycle owner. Its output is a trusted provider-owned credit state that other Adobe skills can consume.
Own here:
- Remote status parsing for free, paid, remaining, balance, and legacy credit fields.
- Preserving
0 as exhausted instead of falling back to defaults.
- User-scoped cache restore/persist rules for credits and billing state.
- Ordinary refresh vs payment-return refresh merge policy.
- PayLink/payment pending markers, baselines, polling, and idempotency.
- Successful-generation deduction, remote usage sync, business-payload failure detection, and rollback.
- Footer credit display as a view of provider state.
Do not make this skill the sole owner of subscription quota semantics. It may normalize subscription-adjacent fields, but subscription-credits-guardrails owns subscribed-state detection, monthly quota math, monthly-first deduction order, and subscribed footer total rules. When both are used, this skill provides freeCount / normalized purchased-credit balance; the subscription skill composes that with monthly quota.
First Moves
- Confirm the active Adobe runtime before editing. Mixed repos may contain dead Canva or Adobe files.
- Read the target files that own billing state and generation:
- provider/user context, often
src/user/index.tsx
- user API wrappers, often
src/user/apis.ts
- pricing link component, often
src/components/footer/pay_link.tsx
- footer/status display, often
src/components/footer/index.tsx
- main generation page, often
src/pages/index.tsx
- app context or global state bridge, often
src/app.tsx
- product config, often
src/app_config.ts
- If a reference repo is provided, compare these same responsibilities instead of copying filenames blindly.
- Ignore
node_modules, dist, and old zip artifacts until verification.
Core Contract
Credits must be normalized into one provider-owned billing state before display or spending. Do not let each page, footer, and API call compute its own balance.
A good provider exposes:
credits
creditLimit
creditUsage
isSubscribed
canUseLimit(cnt?, force?)
use(cnt?, force?)
pollUserStatus(options?)
pollUserStatusSoon(options?)
markPaymentRefreshPending()
Use a structured billing state, adapted to the target repo:
interface BillingState {
freeLimit: number;
freeCount: number;
monthlyUsageLimit: number;
monthlyUsageCount: number;
isSubscribed: boolean;
planName: string;
cycleMonth: string;
}
Rules:
0 is a real exhausted state. Never treat it as missing or fall back to 1, freeTimes, or unlimited.
-1 may mean unlimited only when the target backend explicitly uses that contract.
- Footer and generation gates should read from provider state, not raw remote payloads.
- Provider callbacks such as
onUpdateUserStatus should be ref-backed when they can otherwise retrigger login/status effects.
Remote Status Mapping
Do not rely only on subscription.free_cnt. Paid or purchased credits may arrive under different fields.
Parse in this order:
- Explicit remaining fields:
remaining, remaining_credits, balance, available, available_credits, remain, remain_credits, credits, credit
- Legacy
free_cnt normalized as the current remaining balance
- Paid-credit fields:
additional_credits, paid_credits, credit_additional, credit_balance, and similar
Search subscription, profile, and the status root. Accept numeric strings. Treat missing values as unknown, not as 0 and not as unlimited.
Do not assume a single product-wide conversion rate. Pricing and credit amount are product-specific and are determined by the billing/status backend for the active productName or baseUrl, not by a frontend price table. The add-on frontend must not convert price tiers into credits.
Do not add free_cnt + paid_credits, free_cnt + credit_balance, or free_cnt + additional_credits as a frontend conversion step. In verified Adobe add-on projects, the recharge side already returns the purchased credit amount through status fields; adding parallel fields in the add-on can double the displayed balance, for example a 10-credit recharge showing as 20. Treat paid-credit fields as fallback balance candidates unless backend documentation and a captured product-specific /api/user/status payload prove a different contract.
For normal refresh after login, focus, or Adobe Refresh:
freeCount = Math.min(remoteFreeCount, previousFreeCount);
This prevents stale backend status from restoring credits already deducted locally.
For payment return:
if (remoteFreeCount > paymentBaselineCredits) {
freeCount = Math.max(remoteFreeCount, previousFreeCount, remoteFreeCount - paymentBaselineCredits);
} else {
freeCount = previousFreeCount;
}
paymentBaselineCredits must be the last trusted remote balance before opening pricing, not the local displayed balance. Local display may already include successful-use deductions that the backend has not reflected yet.
Payment-return merge must be idempotent. The same payment status may be observed by immediate PayLink polling, interval polling, focus, pageshow, and visibilitychange. Do not repeatedly apply previousFreeCount + (remoteFreeCount - paymentBaselineCredits) because one 10-credit recharge can become 0 -> 10 -> 20. Treat remoteFreeCount as the backend's current authoritative balance for that payment cycle and merge by max/candidate balance, or clear/cancel pending polling immediately after confirmed growth.
Local Cache
Use one structured, user-scoped billing cache when local persistence is needed. Old global keys are a common cause of stale balances reviving.
Rules:
- Persist the normalized billing state, not scattered raw fields.
- Restore the cached provider-owned billing state before or during initial login. A page reload must not start from
appConfig.freeTimes / freeTimes when a user-scoped cached state says the user has 0 credits.
- If the remote status payload lacks an explicit remaining-credit field, keep the cached/provider
freeCount instead of falling back to freeTimes.
- Initial login/status refresh after reload should use conservative merge behavior such as
preserveDisplayedCredits: true so stale backend status cannot lift an exhausted 0 back to 1.
- Remove or ignore old global credit keys after migration.
- Use legacy keys only for one-time migration when necessary.
- Reset monthly usage by cycle month if the backend status does not already do this.
Good user-scoped cache shape:
const USER_STATUS_CACHE_PREFIX = "<app>_user_status_v1:";
function getUserStatusCacheKey(userId: string) {
return `${USER_STATUS_CACHE_PREFIX}${userId}`;
}
Persist after local optimistic usage and after successful status merge. Read by canonical user id / canva_id before applying fallback profiles. Do not let fallback profiles write free_cnt: freeTimes over a valid cached exhausted state.
Payment Return
Payment refresh is a special mode. It must not share the same merge policy as ordinary refresh.
Recommended payment options:
const PAYMENT_RETURN_REFRESH_OPTIONS = {
preferRemoteCredits: true,
allowRemoteCreditIncrease: true,
maxCount: 1,
refreshOnly: true,
refreshPassportOnEmptyProfile: false,
};
Use three layers:
- PayLink layer:
- mark payment pending with the last trusted remote baseline
- start immediate polling before opening the external pricing page
- run bounded interval polling after click
- listen for
focus, pageshow, and visibilitychange
- Provider payment-opened layer:
- listen for the PayLink event
- schedule the longer payment polling window
- Provider return layer:
- on
focus, pageshow, or visible visibilitychange, choose payment refresh only when pending payment exists
- otherwise run conservative refresh with
preserveDisplayedCredits
Example polling shape:
const PAYMENT_REFRESH_DELAYS = [
0, 1500, 3500, 7000, 12000, 20000, 30000, 45000, 60000, 90000, 120000,
];
Clear payment pending only after confirmed payment growth, expiry, invalid pending data, or before a successful local usage deduction.
When payment growth is confirmed, cancel any outstanding payment polling timers in the same operation that clears pending state. Leaving timers alive can re-enter payment mode with the old baseline and reapply the same purchase.
Spend Flow
Before generation:
- Confirm user status is ready.
- If passport/token is missing, refresh status/passport without rerunning a full login loop.
- Run a conservative status refresh when needed.
- Re-check
canUseLimit() after refresh.
- Block only when trusted state says no credits.
After generation succeeds:
- Apply local optimistic deduction.
- Clear stale payment pending state.
- Sync usage remotely, for example
reduce-free-cnt and/or add-monthly-usage.
- Treat HTTP 200 with a failure payload as failure.
- Roll back local optimistic deduction if usage sync fails.
- Refresh with
preserveDisplayedCredits: true.
Do not deduct for upload, selection, segmentation, cancellation, policy errors, or failed generation.
API Wrapper Requirements
Usage-sync wrappers must inspect business payloads, not only HTTP status.
Treat these as failures:
success: false
- non-success
code, statusCode, errCode, or errorCode
status values such as error, failed, fail, failure
- explicit
error fields
On failure, throw so the provider can roll back optimistic state.
Footer And UI Display
The footer should be a pure view of provider state.
- Free users: show current remaining credits from provider state, for example
Use 1 of {credits}.
- Subscribed users: show monthly usage and limit from normalized billing state.
- For subscribed footer quota messaging, defer to
subscription-credits-guardrails: do not always combine monthly quota and pay-as-you-go credits into one footer total. Show pay-as-you-go only after monthly quota is exhausted, or as an extra line for unlimited subscriptions.
- Canceled-but-active subscription states should be handled separately from free and active subscription states.
- Keep
freeLimit >= freeCount for free-plan display when remote paid credits increase the count.
Diagnostics
Use a temporary, project-specific log prefix while debugging. Search console and network together.
Recommended events:
paylink-click
payment-refresh-pending-marked
poll-user-status-scheduled
poll-user-status-start
poll-user-status-success
poll-user-status-error
usage-deduct-start
usage-deduct-rollback
Network requests to inspect:
- login endpoint
- passport endpoint
- user/status endpoint
- usage decrement endpoint
- monthly usage endpoint
- pricing URL
Interpretation:
- Payment click exists but no polling: PayLink did not start return sync.
- Polling exists but no status request: provider refresh is not wired.
- Status returns paid balance but UI stays stale: parser or React state propagation is wrong.
- Refresh restores spent credits: normal refresh is trusting stale remote status.
- Page reload restores
Use 1 of 1 after spending the last credit: provider state is not persisted, initial login is falling back to freeTimes, or remote status without explicit balance is overwriting cached 0.
- Payment return shows
0: incomplete payment status overwrote local state.
- Payment restores already-spent credits: baseline used local display instead of last trusted remote.
Validation Checklist
Run source checks appropriate to the repo, usually:
npm run lint:types
npm run build
If producing a deliverable:
npm run package
unzip -p dist.zip manifest.json
Validate in Adobe Express, not only a standalone browser:
- Uploading or selecting an input does not change credits.
- A user with
0 credits stays at 0 and is blocked correctly.
- After successful generation spends the last credit, close/reopen or refresh the Adobe panel. The footer must still show
0 remaining, not Use 1 of 1, unless a real payment-return refresh confirmed purchased credits.
- Clicking Generate with credits available does not show a false no-credit alert.
- Successful generation deducts once.
- Failed or canceled generation does not deduct.
- Adobe Refresh does not restore consumed credits.
- Payment return updates credits without manual refresh.
- Payment return adds only purchased credits and does not restore credits consumed before payment.
- Footer numbers match provider state after login, generation, refresh, and payment return.
Reference Case
For the detailed finalized incident that this standard was extracted from, read references/magic-eraser-incident.md only when you need concrete examples from /Users/hodohimehime/Desktop/code/rework/adobe-magic-eraser.