A photo-to-video form has two promises to keep when the user presses Generate. The selected photo is the one the user has permission to process, and the credit quote belongs to the selected mode. Both can change while the page is open. A confirmation checkbox and a disabled button can make the happy path look complete while leaving both promises ambiguous.
Consider an outfit photo that is ready to animate. The user confirms the photo and sees a quote for Fast. Then they switch to Premium, and a new quote starts loading. If the old quote stays visible and the checkbox stays checked, the button can act on a combination the user never approved. Swapping the photo poses a similar problem even when the mode and price stay the same.
Bind confirmation to the state that matters
Assign each selected file an opaque local key when the user chooses it. A filename is a poor key because two different files can have the same name. The key can be an in-memory counter or a random identifier. It needs to change when the selection changes. This UI key should stay local unless your backend contract requires a separate upload identifier.
Treat each quote as a server-issued record with an ID, a mode, a credit cost and an expiry. Capture the current photo key, quote ID and displayed credit amount in the confirmation record. Comparing the cost explicitly also protects the UI against an accidental reuse of a quote ID for a different displayed amount. When the user changes the photo or the selected mode, evaluate the gate again. The view can explain whether it needs input, a refreshed quote or another confirmation.
The example below is a standalone policy for a hypothetical paid generation form. Its identifiers and credit values are synthetic.
export function generationGate(selection, quote, confirmation, now) {
if (!Number.isFinite(now)) throw new TypeError("Expected a finite clock value");
if (!selection?.photoKey || !selection?.mode) return "choose_input";
if (!quote?.id ||
quote.mode !== selection.mode ||
!Number.isSafeInteger(quote.credits) ||
quote.credits < 1 ||
!Number.isFinite(quote.expiresAt) ||
quote.expiresAt <= now) return "refresh_quote";
if (confirmation?.photoKey !== selection.photoKey ||
confirmation?.quoteId !== quote.id ||
confirmation?.credits !== quote.credits) return "confirm_selection";
return "allow_request";
}
The order matters. If the mode no longer matches, prompt for a quote refresh before asking the user to confirm anything. If the quote is current but the photo changed, ask for confirmation against the current selection. A confirmation keyed only by price would miss that change. If the platform separates photo permission from agreement to the cost, store those acknowledgments independently and evaluate both at the same boundary.
Check the boundary with small counterexamples
Start with photo P1, mode Fast, and a valid quote Q1 for 7 test credits. A confirmation of P1 and Q1 should allow the request. Change only the photo key to P2 and expect a new confirmation. Switch the mode with Q1 still in memory and expect a quote refresh. Replace Q1 with Q2 at the same cost and expect a new confirmation. Move the clock exactly to the expiry and expect the quote to be invalid. Also try a reused quote ID with a changed price and confirm the old agreement no longer applies.
These cases make better checks than clicking through a single screen recording. Each changes one variable and gives you an expected recovery path. A rendered price label can look correct while the request state is stale. Run the gate against the actual selection and quote right before constructing the request.
Let the server make the final decision
The browser gate improves the interaction. A server still needs to verify the authenticated user, upload ownership, quote validity and credit balance against its own records. If the quote expires between the click and the server check, return a recoverable response and present the new amount for confirmation. For a retry after a connection drop, use the same request idempotency key for the same operation and let the server look up its result. A genuinely new operation needs a different key.
AIDanceVideo is one example of the product shape motivating the exercise. It uses one authorized JPG or PNG, a fixed Dime Dime Baby dance, and Fast or Premium modes. The sample policy above is independent teaching code. It makes no claim about that site's implementation. With video generation, a successful request also leaves an editorial step. Watch the full silent result, since clothing, hands and facial details can change while the subject moves.
Which state change would you put first in your own test suite: a replaced file, an expired quote, or a retry after the response was lost?