Skip to content

Commit 6862da7

Browse files
Merge pull request #25 from showpass/SPW-20610-2
fix (playground) | Simplify basket reset - SPW-20610
2 parents 11e981a + c563808 commit 6862da7

5 files changed

Lines changed: 42 additions & 16 deletions

File tree

‎scripts/checkout-restart.test.mjs‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,29 @@ const { StaticRouter } = require("react-router-dom/server");
1414
const source = readFileSync(new URL("../src/docs-app/ui/components/widgets/checkout-restart.ts", import.meta.url), "utf8");
1515
const code = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText;
1616

17-
function fixture() {
17+
function fixture({ redirectDirectoryIndex = false } = {}) {
1818
const listeners = new Set();
1919
const timers = new Map();
2020
const replies = [];
2121
const events = [];
2222
const controller = new AbortController();
2323
const window = {
2424
crypto: webcrypto,
25-
location: { origin: "https://dev.showpass.com", reload: () => events.push("reload") },
25+
location: {
26+
origin: "https://dev.showpass.com",
27+
reload() {
28+
events.push("reload");
29+
const url = new URL(window.location.href);
30+
if (redirectDirectoryIndex && url.pathname === "/sdk/widget-playground") {
31+
// GitHub Pages adds the directory's trailing slash. HTTP redirects
32+
// clear history state, including the single-use reset click marker.
33+
url.pathname += "/";
34+
window.location.href = url.toString();
35+
window.history.state = null;
36+
events.push("redirect");
37+
}
38+
},
39+
},
2640
history: {
2741
state: { idx: 0 },
2842
replaceState(state, title, url) { this.state = state; if (url) window.location.href = new URL(url, window.location.origin).toString(); },
@@ -51,7 +65,7 @@ test("navigation retains only public settings and strips tokens and reset state"
5165
const f = fixture();
5266
const search = new URLSearchParams({ environment: "demo", eventId: "event", membershipId: "membership", theme: "#24727b", tab: "mounted", widget: "event", customer_attribution_token: "SECRET", token: "SECRET", returnUrl: "https://evil.example", checkoutAction: "reset" });
5367
const resetUrl = new URL(f.getCheckoutResetUrl(search, "reset"), f.window.location.origin);
54-
assert.equal(resetUrl.pathname, "/sdk/widget-playground");
68+
assert.equal(resetUrl.pathname, "/sdk/widget-playground/");
5569
assert.equal(resetUrl.searchParams.get("memberId"), "membership");
5670
assert.equal(resetUrl.searchParams.get("checkoutAction"), "reset");
5771
assert.equal(resetUrl.searchParams.get("environment"), "demo");
@@ -69,7 +83,7 @@ test("a button click reloads the Playground with a single-use reset intent and n
6983
assert.equal(f.consumeCheckoutResetIntent(), false);
7084
f.startCheckoutReset(new URLSearchParams({ environment: "demo", eventId: "test-event", customer_attribution_token: "SECRET" }));
7185
const url = new URL(f.window.location.href);
72-
assert.equal(url.pathname, "/sdk/widget-playground");
86+
assert.equal(url.pathname, "/sdk/widget-playground/");
7387
assert.equal(url.searchParams.get("checkoutAction"), "reset");
7488
assert.ok(!f.window.location.href.includes("SECRET"));
7589
assert.ok(!JSON.stringify(f.window.history.state).includes("SECRET"));
@@ -88,6 +102,17 @@ test("a completed reset retains public settings without starting another reset",
88102
assert.equal(f.consumeCheckoutResetIntent(), false);
89103
});
90104

105+
test("the first reset click survives a static-host reload without losing its intent", () => {
106+
const f = fixture({ redirectDirectoryIndex: true });
107+
for (let attempt = 0; attempt < 3; attempt++) {
108+
f.startCheckoutReset(new URLSearchParams({ environment: "demo", eventId: "test-event" }));
109+
assert.equal(f.consumeCheckoutResetIntent(), true, "the first click must authorize reset after reload");
110+
assert.equal(f.consumeCheckoutResetIntent(), false, "the intent must still be single-use");
111+
assert.equal(new URL(f.window.location.href).pathname, "/sdk/widget-playground/");
112+
}
113+
assert.deepEqual(f.events, ["reload", "reload", "reload"]);
114+
});
115+
91116
test("expired or malformed history markers cannot authorize reset", () => {
92117
const f = fixture();
93118
for (const timestamp of [Date.now() - 61_000, Date.now() + 61_000, true, "reset"]) {

‎src/docs-app/data/sdk/02-ticket-purchase-widget.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,15 +70,15 @@ Open the [Widget Playground](/sdk/widget-playground), select the same environmen
7070

7171
The widget forwards the token to Showpass checkout, which includes it in the basket request body. The playground keeps the token in memory and clears it on reload or environment change. Use a fresh token after its one-hour expiry; do not paste your Partner credential here. Close and reopen an existing modal after changing the applied token.
7272

73-
**If you are signed in, your Showpass account must match the token’s customer.** To test different customers, use an incognito window and stay signed out. Reset basket keeps the current login.
73+
**If you are signed in, your Showpass account must match the token’s customer.** To test different customers, use an incognito window and stay signed out. Clearing and resetting keeps the current login.
7474

7575
### Reset a test checkout in the Playground
7676

77-
Applying or clearing a token changes the Playground setting. It does not discard items or change the customer attached to an existing Showpass basket. **Clear token** keeps that basket.
77+
Applying a token changes the Playground setting and keeps the existing basket and customer. **Clear token & reset basket** removes both the pasted and applied token and expires that basket so you can test another customer.
7878

79-
Use **Reset basket** beside **Apply token** and **Clear token**, inside **Partner customer**, before testing another customer:
79+
Use **Clear token & reset basket** beside **Apply token**, inside **Partner customer**, before testing another customer:
8080

81-
1. Click **Reset basket**. It immediately starts resetting the basket, with progress shown below the token controls. There is no separate confirmation page or dialog.
81+
1. Click **Clear token & reset basket**. It immediately starts resetting the basket, with progress shown below the token controls. There is no separate confirmation page or dialog.
8282
2. The Playground refreshes to unload old widgets and clears both the pasted and applied token. Your public settings are kept. The old basket is expired and its reservations released.
8383
3. When **Basket reset** appears, paste and apply the next customer's token, then select items to create a fresh basket. Leave the token empty to try ordinary checkout.
8484

‎src/docs-app/ui/components/widgets/PlaygroundCheckoutReset.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from "react";
2+
import { Loader2 } from "lucide-react";
23
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
34
import { Button } from "@/shared/components/button";
45
import { consumeCheckoutResetIntent, getCheckoutResetUrl, runCheckoutRestart, startCheckoutReset } from "./checkout-restart";
@@ -20,6 +21,7 @@ export default function PlaygroundCheckoutReset({ environment, disabled }: Playg
2021
const authorized = React.useRef(false);
2122
const [attempt, setAttempt] = React.useState(0);
2223
const [error, setError] = React.useState("");
24+
const busy = resetting && !error;
2325

2426
React.useEffect(() => {
2527
const frame = iframe.current;
@@ -60,8 +62,9 @@ export default function PlaygroundCheckoutReset({ environment, disabled }: Playg
6062
};
6163

6264
return <>
63-
<Button size="sm" variant="outline" disabled={disabled || (resetting && !error)} onClick={reset}>
64-
{error ? "Retry reset" : "Reset basket"}
65+
<Button size="sm" variant="outline" disabled={disabled || busy} aria-busy={busy} onClick={reset}>
66+
{busy && <Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" aria-hidden="true" />}
67+
{error ? "Retry reset" : busy ? "Resetting basket…" : "Clear token & reset basket"}
6568
</Button>
6669
{resetting && <iframe ref={iframe} title="Reset test basket" hidden allow="storage-access *" referrerPolicy="origin" />}
6770
{error ? <p role="alert" className="w-full text-xs text-destructive">{error}</p>

‎src/docs-app/ui/components/widgets/checkout-restart.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ export function getCheckoutResetUrl(search: URLSearchParams, action?: "reset"):
1717
}
1818
if (action) query.set("checkoutAction", action);
1919
const queryString = query.toString();
20-
return `/sdk/widget-playground${queryString ? `?${queryString}` : ""}`;
20+
// GitHub Pages redirects directory URLs without a trailing slash. Avoid that
21+
// redirect on reload because it clears the history marker for the reset click.
22+
return `/sdk/widget-playground/${queryString ? `?${queryString}` : ""}`;
2123
}
2224

2325
export function startCheckoutReset(search: URLSearchParams): void {

‎src/pages/WidgetPlayground.tsx‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ const WidgetPlayground: React.FC = () => {
108108
const [eventError, setEventError] = React.useState("");
109109
const [isOpeningEvent, setIsOpeningEvent] = React.useState(false);
110110
const openingEvent = React.useRef(false);
111-
const [checkoutStatus, setCheckoutStatus] = React.useState("");
112111
const onSdkReady = React.useCallback(() => setSdkReady(true), []);
113112

114113
const changeEnvironment = (value: string) => {
@@ -275,14 +274,12 @@ const WidgetPlayground: React.FC = () => {
275274
<div className="flex flex-wrap gap-2">
276275
<Button size="sm" disabled={resetting || !tokenInput.trim() || !!tokenError || tokenInput.trim() === customerAttributionToken}
277276
onClick={() => setCustomerAttributionToken(tokenInput.trim())}>Apply token</Button>
278-
<Button size="sm" variant="outline" disabled={resetting || (!tokenInput && !customerAttributionToken)}
279-
onClick={() => { setTokenInput(""); setCustomerAttributionToken(""); setCheckoutStatus("Token cleared. The existing basket is kept. Use Reset basket to discard it."); }}>Clear token</Button>
280277
<PlaygroundCheckoutReset environment={environment} disabled={isOpeningEvent} />
281278
</div>
282279
{tokenError && <p role="alert" className="text-xs text-destructive">{tokenError}</p>}
283280
{tokenInput.trim() !== customerAttributionToken && !tokenError && <p className="text-xs text-muted-foreground">Your changes have not been applied yet.</p>}
284281
<p role="status" className="text-xs">{customerAttributionToken ? "Token applied to Event Tickets." : "No token applied. You can still try normal checkout."}</p>
285-
<p className="text-xs text-muted-foreground">Apply/Clear token keeps the current basket. Reset basket clears it and your token, then refreshes the Playground. Your login stays the same.</p>
282+
<p className="text-xs text-muted-foreground">Applying a token keeps the existing basket. Clearing and resetting removes the token and expires that basket. Your login stays the same.</p>
286283
{environment === "prod" && <p className="text-xs text-destructive">Production reset releases real reservations, including those shared with other widgets.</p>}
287284
</div>
288285
</details>
@@ -331,7 +328,6 @@ const WidgetPlayground: React.FC = () => {
331328
</div>
332329
</div>
333330
</section>
334-
{checkoutStatus && <p role="status" className="mb-4 text-sm">{checkoutStatus}</p>}
335331
{sdkError && <p role="alert" className="mb-4 text-destructive">{sdkError} Check that this environment is running and reload the page.</p>}
336332
{!resetting && !sdkReady && !sdkError && <p role="status" className="mb-4">Loading Showpass widgets…</p>}
337333

0 commit comments

Comments
 (0)