Skip to content

Commit 564b0e3

Browse files
Larkooobroody
authored andcommitted
Add starterpack play callback (#2362)
Adds an onPlay callback option to starterpack flows and routes Play clicks through it. The keychain iframe now closes before calling the callback and falls back to the existing close behavior when absent.
1 parent ffdac0c commit 564b0e3

File tree

12 files changed

+167
-37
lines changed

12 files changed

+167
-37
lines changed

examples/next/src/components/Profile.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,15 @@ export function Profile() {
7474
<h2>Open Starterpack</h2>
7575
<div className="flex flex-col gap-1">
7676
<div className="flex flex-wrap gap-1">
77-
<Button onClick={() => ctrlConnector.controller.openStarterPack(0)}>
77+
<Button
78+
onClick={() =>
79+
ctrlConnector.controller.openStarterPack(0, {
80+
onPurchaseComplete: () => {
81+
console.log("Starterpack play callback fired.");
82+
},
83+
})
84+
}
85+
>
7886
Nums
7987
</Button>
8088
</div>

examples/next/src/components/Starterpack.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ export const Starterpack = () => {
8484
onClick={() => {
8585
controllerConnector.controller.openStarterPack(
8686
purchaseOnchainSpId,
87+
{
88+
onPurchaseComplete: () => {
89+
console.log("Starterpack play callback fired.");
90+
},
91+
},
8792
);
8893
}}
8994
>

packages/controller/src/controller.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,14 @@ export default class ControllerProvider extends BaseProvider {
459459
return;
460460
}
461461

462-
await this.keychain.openStarterPack(id, options);
462+
const { onPurchaseComplete, ...starterpackOptions } = options ?? {};
463+
this.iframes.keychain.setOnStarterpackPlay(onPurchaseComplete);
464+
const sanitizedOptions =
465+
Object.keys(starterpackOptions).length > 0
466+
? (starterpackOptions as Omit<StarterpackOptions, "onPurchaseComplete">)
467+
: undefined;
468+
469+
await this.keychain.openStarterPack(id, sanitizedOptions);
463470
this.iframes.keychain?.open();
464471
}
465472

packages/controller/src/iframe/keychain.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,15 @@ type KeychainIframeOptions = IFrameOptions<Keychain> &
1111
needsSessionCreation?: boolean;
1212
username?: string;
1313
onSessionCreated?: () => void;
14+
onStarterpackPlay?: () => void;
1415
encryptedBlob?: string;
1516
};
1617

18+
const STARTERPACK_PLAY_CALLBACK_DELAY_MS = 200;
19+
1720
export class KeychainIFrame extends IFrame<Keychain> {
1821
private walletBridge: WalletBridge;
22+
private onStarterpackPlay?: () => void;
1923

2024
constructor({
2125
url,
@@ -32,11 +36,13 @@ export class KeychainIFrame extends IFrame<Keychain> {
3236
needsSessionCreation,
3337
username,
3438
onSessionCreated,
39+
onStarterpackPlay,
3540
encryptedBlob,
3641
propagateSessionErrors,
3742
errorDisplayMode,
3843
...iframeOptions
3944
}: KeychainIframeOptions) {
45+
let onStarterpackPlayHandler: (() => Promise<void>) | undefined;
4046
const _url = new URL(url ?? KEYCHAIN_URL);
4147
const walletBridge = new WalletBridge();
4248

@@ -118,10 +124,32 @@ export class KeychainIFrame extends IFrame<Keychain> {
118124
onSessionCreated();
119125
}
120126
},
127+
onStarterpackPlay: (_origin: string) => async () => {
128+
if (onStarterpackPlayHandler) {
129+
await onStarterpackPlayHandler();
130+
}
131+
},
121132
},
122133
});
123134

124135
this.walletBridge = walletBridge;
136+
this.onStarterpackPlay = onStarterpackPlay;
137+
onStarterpackPlayHandler = async () => {
138+
this.close();
139+
const callback = this.onStarterpackPlay;
140+
this.onStarterpackPlay = undefined;
141+
if (!callback) {
142+
return;
143+
}
144+
await new Promise((resolve) =>
145+
setTimeout(resolve, STARTERPACK_PLAY_CALLBACK_DELAY_MS),
146+
);
147+
try {
148+
callback();
149+
} catch (error) {
150+
console.error("Failed to run starterpack play callback:", error);
151+
}
152+
};
125153

126154
// Expose the wallet bridge instance globally for WASM interop
127155
if (typeof window !== "undefined") {
@@ -132,4 +160,8 @@ export class KeychainIFrame extends IFrame<Keychain> {
132160
getWalletBridge(): WalletBridge {
133161
return this.walletBridge;
134162
}
163+
164+
setOnStarterpackPlay(callback?: () => void) {
165+
this.onStarterpackPlay = callback;
166+
}
135167
}

packages/controller/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,4 +273,6 @@ export type OpenOptions = {
273273
export type StarterpackOptions = {
274274
/** The preimage to use */
275275
preimage?: string;
276+
/** Callback fired after the Play button closes the starterpack modal */
277+
onPurchaseComplete?: () => void;
276278
};

packages/keychain/src/components/purchasenew/pending/base.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import { Receiving } from "../receiving";
88
import { ConfirmingTransaction } from "./confirming-transaction";
99
import { Item } from "@/context";
1010
import { useConnection } from "@/hooks/connection";
11-
import { useNavigation } from "@/context";
1211
import { useEffect, useState } from "react";
1312
import { TransactionFinalityStatus } from "starknet";
1413
import { retryWithBackoff } from "@/utils/retry";
1514
import { getExplorer } from "@/hooks/starterpack/layerswap";
15+
import { useStarterpackPlayHandler } from "@/hooks/starterpack";
1616

1717
export interface TransactionPendingBaseProps {
1818
/** Header title (e.g., "Purchasing Village Kit") */
@@ -44,8 +44,8 @@ export function TransactionPendingBase({
4444
buttonText,
4545
quantityText,
4646
}: TransactionPendingBaseProps) {
47-
const { isMainnet, controller, closeModal } = useConnection();
48-
const { navigateToRoot } = useNavigation();
47+
const { isMainnet, controller } = useConnection();
48+
const handlePlay = useStarterpackPlayHandler();
4949
const [isPending, setIsPending] = useState(true);
5050

5151
useEffect(() => {
@@ -93,10 +93,7 @@ export function TransactionPendingBase({
9393
className="w-full"
9494
variant="primary"
9595
disabled={isPending}
96-
onClick={() => {
97-
closeModal?.();
98-
navigateToRoot();
99-
}}
96+
onClick={handlePlay}
10097
>
10198
{buttonText}
10299
</Button>

packages/keychain/src/components/purchasenew/pending/bridge.tsx

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ import {
1515
import { Explorer, getExplorer } from "@/hooks/starterpack/layerswap";
1616
import { ExternalWallet, humanizeString } from "@cartridge/controller";
1717
import { useEffect, useState, useRef } from "react";
18-
import { useNavigation } from "@/context";
1918
import { useConnection } from "@/hooks/connection";
2019
import { retryWithBackoff } from "@/utils/retry";
2120
import { ControllerErrorAlert } from "@/components/ErrorAlert";
2221
import { TransactionFinalityStatus } from "starknet";
2322
import { CoinbaseTransactionStatus } from "@/utils/api";
23+
import { useStarterpackPlayHandler } from "@/hooks/starterpack";
2424

2525
interface TransitionStepProps {
2626
isVisible: boolean;
@@ -73,11 +73,10 @@ export function BridgePending({
7373
selectedPlatform: selectedPlatformProp,
7474
waitForDeposit: waitForDepositProp,
7575
}: BridgePendingProps) {
76-
const { navigateToRoot } = useNavigation();
7776
const onchainContext = useOnchainPurchaseContext();
7877
const { transactionHash: currentTxHash } = useStarterpackContext();
79-
const { externalWaitForTransaction, controller, isMainnet, closeModal } =
80-
useConnection();
78+
const { externalWaitForTransaction, controller, isMainnet } = useConnection();
79+
const handlePlay = useStarterpackPlayHandler();
8180

8281
// Use props if provided (for stories), otherwise use context
8382
const selectedPlatform =
@@ -282,10 +281,7 @@ export function BridgePending({
282281
className="w-full"
283282
variant="primary"
284283
disabled={!purchaseCompleted}
285-
onClick={() => {
286-
closeModal?.();
287-
navigateToRoot();
288-
}}
284+
onClick={handlePlay}
289285
>
290286
Play
291287
</Button>

packages/keychain/src/components/purchasenew/success.tsx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
import { Receiving } from "./receiving";
99
import { useConnection } from "@/hooks/connection";
1010
import {
11-
useNavigation,
1211
useStarterpackContext,
1312
useOnchainPurchaseContext,
1413
Item,
@@ -17,6 +16,7 @@ import { useMemo } from "react";
1716
import { ConfirmingTransaction } from "./pending";
1817
import { getExplorer } from "@/hooks/starterpack/layerswap";
1918
import { StarterpackType } from "@/context";
19+
import { useStarterpackPlayHandler } from "@/hooks/starterpack";
2020

2121
export function Success() {
2222
const { starterpackDetails, transactionHash, claimItems } =
@@ -50,8 +50,8 @@ export function PurchaseSuccessInner({
5050
transactionHash?: string;
5151
}) {
5252
const { quantity } = useOnchainPurchaseContext();
53-
const { closeModal, isMainnet } = useConnection();
54-
const { navigateToRoot } = useNavigation();
53+
const { isMainnet } = useConnection();
54+
const handlePlay = useStarterpackPlayHandler();
5555
const quantityText = quantity > 1 ? `(${quantity})` : "";
5656

5757
return (
@@ -78,14 +78,7 @@ export function PurchaseSuccessInner({
7878
isLoading={false}
7979
/>
8080
)}
81-
<Button
82-
onClick={() => {
83-
closeModal?.();
84-
navigateToRoot();
85-
}}
86-
>
87-
Play
88-
</Button>
81+
<Button onClick={handlePlay}>Play</Button>
8982
</LayoutFooter>
9083
</>
9184
);

packages/keychain/src/hooks/connection.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ export type ParentMethods = AsyncMethodReturns<{
111111

112112
// Session creation callback (for standalone auth flow)
113113
onSessionCreated?: () => Promise<void>;
114+
115+
// Starterpack play callback (for purchase completion flow)
116+
onStarterpackPlay?: () => Promise<void>;
114117
}>;
115118

116119
/**
@@ -426,11 +429,6 @@ export function useConnectionValue() {
426429
return;
427430
}
428431

429-
if (!configData.origin) {
430-
setVerified(false);
431-
return;
432-
}
433-
434432
const allowedOrigins = toArray(configData.origin as string | string[]);
435433

436434
// In standalone mode (not iframe), verify preset if redirect_url matches preset whitelist
@@ -443,8 +441,10 @@ export function useConnectionValue() {
443441
const redirectUrlObj = new URL(redirectUrl);
444442
const redirectOrigin = redirectUrlObj.origin;
445443

446-
// Always consider localhost as verified for development
447-
const isLocalhost = redirectOrigin.includes("localhost");
444+
// Always consider localhost and capacitor as verified for development
445+
const isLocalhost =
446+
redirectOrigin.includes("localhost") ||
447+
redirectOrigin.startsWith("capacitor://");
448448
const isOriginAllowed = isOriginVerified(
449449
redirectOrigin,
450450
allowedOrigins,
@@ -463,10 +463,16 @@ export function useConnectionValue() {
463463
return;
464464
}
465465

466+
if (!configData.origin) {
467+
setVerified(false);
468+
return;
469+
}
470+
466471
// Embedded mode: verify against parent origin
467-
// Always consider localhost as verified for development (not 127.0.0.1)
472+
// Always consider localhost and capacitor as verified for development (not 127.0.0.1)
468473
if (origin) {
469-
const isLocalhost = origin.includes("localhost");
474+
const isLocalhost =
475+
origin.includes("localhost") || origin.startsWith("capacitor://");
470476
const isOriginAllowed = isOriginVerified(origin, allowedOrigins);
471477
const finalVerified = isLocalhost || isOriginAllowed;
472478
setVerified(finalVerified);

packages/keychain/src/hooks/starterpack/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,5 @@ export type {
4242
CoinbaseTransactionResult,
4343
CoinbaseQuoteResult,
4444
} from "./coinbase";
45+
46+
export { useStarterpackPlayHandler } from "./play";

0 commit comments

Comments
 (0)