@cartridge/controller 0.13.10-alpha.1 → 0.13.11-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.ts CHANGED
@@ -13,11 +13,11 @@ export type KeychainSession = {
13
13
  privateKey: string;
14
14
  };
15
15
  };
16
- export declare const EMBEDDED_WALLETS: readonly ["google", "webauthn", "discord", "walletconnect", "password"];
16
+ export declare const EMBEDDED_WALLETS: readonly ["google", "webauthn", "discord", "walletconnect", "password", "sms"];
17
17
  export type EmbeddedWallet = (typeof EMBEDDED_WALLETS)[number];
18
- export declare const ALL_AUTH_OPTIONS: readonly ["google", "webauthn", "discord", "walletconnect", "password", "metamask", "rabby", "phantom-evm", "argent", "braavos", "phantom", "base"];
18
+ export declare const ALL_AUTH_OPTIONS: readonly ["google", "webauthn", "discord", "walletconnect", "password", "sms", "metamask", "rabby", "phantom-evm", "argent", "braavos", "phantom", "base"];
19
19
  export type AuthOption = (typeof ALL_AUTH_OPTIONS)[number];
20
- export declare const IMPLEMENTED_AUTH_OPTIONS: ("metamask" | "rabby" | "phantom-evm" | "google" | "webauthn" | "discord" | "walletconnect" | "password")[];
20
+ export declare const IMPLEMENTED_AUTH_OPTIONS: ("metamask" | "rabby" | "phantom-evm" | "google" | "webauthn" | "discord" | "walletconnect" | "password" | "sms")[];
21
21
  export type AuthOptions = (typeof IMPLEMENTED_AUTH_OPTIONS)[number][];
22
22
  export declare enum ResponseCodes {
23
23
  SUCCESS = "SUCCESS",
@@ -102,6 +102,7 @@ export interface Keychain {
102
102
  openPurchaseCredits(): void;
103
103
  openExecute(calls: Call[]): Promise<void>;
104
104
  switchChain(rpcUrl: string): Promise<void>;
105
+ openBundle(id: number, registryAddress: string, options?: BundleOptions): Promise<void>;
105
106
  openStarterPack(id: string | number, options?: StarterpackOptions): Promise<void>;
106
107
  navigate(path: string): Promise<void>;
107
108
  externalDetectWallets(): Promise<ExternalWallet[]>;
@@ -162,6 +163,8 @@ export type KeychainOptions = IFrameOptions & {
162
163
  tokens?: Tokens;
163
164
  /** When true, defer iframe mounting until connect() is called. Reduces initial load and resource fetching. */
164
165
  lazyload?: boolean;
166
+ /** When true, force WebAuthn operations to run in a popup window instead of the iframe. Useful for development and testing. */
167
+ webauthnPopup?: boolean;
165
168
  };
166
169
  export type ProfileContextTypeVariant = "inventory" | "trophies" | "achievements" | "quests" | "leaderboard" | "activity";
167
170
  export type Token = "eth" | "strk" | "lords" | "usdc" | "usdt";
@@ -172,6 +175,15 @@ export type OpenOptions = {
172
175
  /** The URL to redirect to after authentication (defaults to current page) */
173
176
  redirectUrl?: string;
174
177
  };
178
+ export type SocialClaimOptions = {
179
+ shareMessage: string;
180
+ };
181
+ export type BundleOptions = {
182
+ /** Callback fired after the Play button closes the starterpack modal */
183
+ onPurchaseComplete?: () => void;
184
+ /** Options for social claim conditional starterpack */
185
+ socialClaimOptions?: SocialClaimOptions;
186
+ };
175
187
  export type StarterpackOptions = {
176
188
  /** The preimage to use */
177
189
  preimage?: string;
package/dist/utils.d.ts CHANGED
@@ -23,5 +23,4 @@ export declare function toWasmPolicies(policies: ParsedSessionPolicies): Policy[
23
23
  export declare function toArray<T>(val: T | T[]): T[];
24
24
  export declare function humanizeString(str: string): string;
25
25
  export declare function parseChainId(url: URL): ChainId;
26
- export declare function isMobile(): boolean;
27
26
  export declare function sanitizeImageSrc(src: string): string;
@@ -6,12 +6,16 @@ export declare abstract class EthereumWalletBase implements WalletAdapter {
6
6
  abstract readonly displayName: string;
7
7
  platform: ExternalPlatform | undefined;
8
8
  protected account: string | undefined;
9
- protected store: import('mipd').Store;
10
9
  protected provider: EIP6963ProviderDetail | undefined;
11
10
  protected connectedAccounts: string[];
12
11
  constructor();
13
12
  private getProvider;
14
13
  private getEthereumProvider;
14
+ /**
15
+ * Fallback provider detection when EIP-6963 announcement is missed.
16
+ * Subclasses can override to provide wallet-specific fallback logic.
17
+ */
18
+ protected getFallbackProvider(): any;
15
19
  private initializeIfAvailable;
16
20
  private initialized;
17
21
  private initializeProvider;
@@ -4,4 +4,5 @@ export declare class MetaMaskWallet extends EthereumWalletBase {
4
4
  readonly type: ExternalWalletType;
5
5
  readonly rdns = "io.metamask";
6
6
  readonly displayName = "MetaMask";
7
+ protected getFallbackProvider(): any;
7
8
  }
@@ -4,4 +4,5 @@ export declare class PhantomEVMWallet extends EthereumWalletBase {
4
4
  readonly type: ExternalWalletType;
5
5
  readonly rdns = "app.phantom";
6
6
  readonly displayName = "Phantom";
7
+ protected getFallbackProvider(): any;
7
8
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartridge/controller",
3
- "version": "0.13.10-alpha.1",
3
+ "version": "0.13.11-alpha.1",
4
4
  "description": "Cartridge Controller",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,15 +26,14 @@
26
26
  }
27
27
  },
28
28
  "dependencies": {
29
- "@cartridge/controller-wasm": "0.9.4",
29
+ "@cartridge/controller-wasm": "0.10.0",
30
30
  "@cartridge/penpal": "^6.2.4",
31
31
  "micro-sol-signer": "^0.5.0",
32
32
  "bs58": "^6.0.0",
33
33
  "ethers": "^6.13.5",
34
34
  "@starknet-io/get-starknet-wallet-standard": "5.0.0-beta.0",
35
35
  "@starknet-io/types-js": "0.9.1",
36
- "@telegram-apps/sdk": "^2.4.0",
37
- "@turnkey/sdk-browser": "^4.0.0",
36
+ "@turnkey/sdk-browser": "^5.15.2",
38
37
  "cbor-x": "^1.5.0",
39
38
  "starknet": "^8.5.2",
40
39
  "mipd": "^0.0.7",
@@ -56,7 +55,7 @@
56
55
  "vite-plugin-node-polyfills": "^0.23.0",
57
56
  "vite-plugin-top-level-await": "^1.4.4",
58
57
  "vite-plugin-wasm": "^3.4.1",
59
- "@cartridge/tsconfig": "0.13.10-alpha.1"
58
+ "@cartridge/tsconfig": "0.13.11-alpha.1"
60
59
  },
61
60
  "scripts": {
62
61
  "build:deps": "pnpm build:browser && pnpm build:node",
package/src/controller.ts CHANGED
@@ -32,6 +32,7 @@ import {
32
32
  HeadlessUsernameLookupResult,
33
33
  StarterpackOptions,
34
34
  UpdateSessionOptions,
35
+ BundleOptions,
35
36
  } from "./types";
36
37
  import { validateRedirectUrl } from "./url-validator";
37
38
  import { parseChainId } from "./utils";
@@ -309,8 +310,9 @@ export default class ControllerProvider extends BaseProvider {
309
310
  return this.account;
310
311
  }
311
312
 
312
- // Only open modal if NOT headless
313
- this.iframes.keychain.open();
313
+ if (!headless) {
314
+ this.iframes.keychain.open();
315
+ }
314
316
 
315
317
  // Use connect() parameter if provided, otherwise fall back to constructor options
316
318
  const effectiveOptions = Array.isArray(options)
@@ -353,7 +355,6 @@ export default class ControllerProvider extends BaseProvider {
353
355
  }
354
356
  console.log(e);
355
357
  } finally {
356
- // Only close modal if it was opened (not headless)
357
358
  if (!headless) {
358
359
  this.iframes.keychain.close();
359
360
  }
@@ -391,7 +392,6 @@ export default class ControllerProvider extends BaseProvider {
391
392
 
392
393
  async disconnect() {
393
394
  this.account = undefined;
394
- this.emitAccountsChanged([]);
395
395
 
396
396
  try {
397
397
  if (typeof localStorage !== "undefined") {
@@ -413,8 +413,13 @@ export default class ControllerProvider extends BaseProvider {
413
413
  return;
414
414
  }
415
415
 
416
+ // Disconnect the keychain (clears iframe localStorage) before emitting
417
+ // state changes, because emitAccountsChanged can trigger framework
418
+ // re-renders that navigate or reload the page.
416
419
  await this.keychain.disconnect();
417
- return this.close();
420
+ this.close();
421
+
422
+ this.emitAccountsChanged([]);
418
423
  }
419
424
 
420
425
  async openProfile(tab: ProfileContextTypeVariant = "inventory") {
@@ -607,6 +612,31 @@ export default class ControllerProvider extends BaseProvider {
607
612
  });
608
613
  }
609
614
 
615
+ async openBundle(
616
+ id: number,
617
+ registryAddress: string,
618
+ options?: BundleOptions,
619
+ ): Promise<void> {
620
+ if (!this.iframes) {
621
+ return;
622
+ }
623
+
624
+ if (!this.keychain || !this.iframes.keychain) {
625
+ console.error(new NotReadyToConnect().message);
626
+ return;
627
+ }
628
+
629
+ const { onPurchaseComplete, ...bundleOptions } = options ?? {};
630
+ this.iframes.keychain.setOnStarterpackPlay(onPurchaseComplete);
631
+ const sanitizedOptions =
632
+ Object.keys(bundleOptions).length > 0
633
+ ? (bundleOptions as Omit<BundleOptions, "onPurchaseComplete">)
634
+ : undefined;
635
+
636
+ await this.keychain.openBundle(id, registryAddress, sanitizedOptions);
637
+ this.iframes.keychain?.open();
638
+ }
639
+
610
640
  async openStarterPack(
611
641
  id: string | number,
612
642
  options?: StarterpackOptions,
@@ -40,6 +40,7 @@ export class KeychainIFrame extends IFrame<Keychain> {
40
40
  encryptedBlob,
41
41
  propagateSessionErrors,
42
42
  errorDisplayMode,
43
+ webauthnPopup,
43
44
  ...iframeOptions
44
45
  }: KeychainIframeOptions) {
45
46
  let onStarterpackPlayHandler: (() => Promise<void>) | undefined;
@@ -101,6 +102,10 @@ export class KeychainIFrame extends IFrame<Keychain> {
101
102
  _url.searchParams.set("should_override_preset_policies", "true");
102
103
  }
103
104
 
105
+ if (webauthnPopup) {
106
+ _url.searchParams.set("webauthn_popup", "true");
107
+ }
108
+
104
109
  // Policy precedence logic:
105
110
  // 1. If shouldOverridePresetPolicies is true and policies are provided, use policies
106
111
  // 2. Otherwise, if preset is defined, ignore provided policies
package/src/lookup.ts CHANGED
@@ -119,10 +119,13 @@ const HEADLESS_AUTH_OPTIONS: AuthOption[] = [
119
119
  "discord",
120
120
  "walletconnect",
121
121
  "password",
122
+ "sms",
122
123
  "metamask",
123
124
  "rabby",
124
125
  "phantom-evm",
125
- ].filter((option) => IMPLEMENTED_AUTH_OPTIONS.includes(option));
126
+ ].filter((option) =>
127
+ IMPLEMENTED_AUTH_OPTIONS.includes(option as AuthOption),
128
+ ) as AuthOption[];
126
129
 
127
130
  function normalizeSignerOptions(
128
131
  signers: LookupSigner[] | undefined,
package/src/types.ts CHANGED
@@ -38,6 +38,7 @@ export const EMBEDDED_WALLETS = [
38
38
  "discord",
39
39
  "walletconnect",
40
40
  "password",
41
+ "sms",
41
42
  ] as const;
42
43
 
43
44
  export type EmbeddedWallet = (typeof EMBEDDED_WALLETS)[number];
@@ -171,6 +172,11 @@ export interface Keychain {
171
172
  openPurchaseCredits(): void;
172
173
  openExecute(calls: Call[]): Promise<void>;
173
174
  switchChain(rpcUrl: string): Promise<void>;
175
+ openBundle(
176
+ id: number,
177
+ registryAddress: string,
178
+ options?: BundleOptions,
179
+ ): Promise<void>;
174
180
  openStarterPack(
175
181
  id: string | number,
176
182
  options?: StarterpackOptions,
@@ -259,6 +265,8 @@ export type KeychainOptions = IFrameOptions & {
259
265
  tokens?: Tokens;
260
266
  /** When true, defer iframe mounting until connect() is called. Reduces initial load and resource fetching. */
261
267
  lazyload?: boolean;
268
+ /** When true, force WebAuthn operations to run in a popup window instead of the iframe. Useful for development and testing. */
269
+ webauthnPopup?: boolean;
262
270
  };
263
271
 
264
272
  export type ProfileContextTypeVariant =
@@ -280,6 +288,17 @@ export type OpenOptions = {
280
288
  redirectUrl?: string;
281
289
  };
282
290
 
291
+ export type SocialClaimOptions = {
292
+ shareMessage: string;
293
+ };
294
+
295
+ export type BundleOptions = {
296
+ /** Callback fired after the Play button closes the starterpack modal */
297
+ onPurchaseComplete?: () => void;
298
+ /** Options for social claim conditional starterpack */
299
+ socialClaimOptions?: SocialClaimOptions;
300
+ };
301
+
283
302
  export type StarterpackOptions = {
284
303
  /** The preimage to use */
285
304
  preimage?: string;
package/src/utils.ts CHANGED
@@ -256,14 +256,6 @@ export function parseChainId(url: URL): ChainId {
256
256
  throw new Error(`Chain ${url.toString()} not supported`);
257
257
  }
258
258
 
259
- export function isMobile() {
260
- return (
261
- window.matchMedia("(max-width: 768px)").matches ||
262
- "ontouchstart" in window ||
263
- navigator.maxTouchPoints > 0
264
- );
265
- }
266
-
267
259
  // Sanitize image src to prevent XSS
268
260
  export function sanitizeImageSrc(src: string): string {
269
261
  // Allow only http/https URLs (absolute)
@@ -1,6 +1,5 @@
1
1
  import { getAddress } from "ethers/address";
2
- import { createStore, EIP6963ProviderDetail } from "mipd";
3
- import { isMobile } from "../utils";
2
+ import { createStore, EIP6963ProviderDetail, Store } from "mipd";
4
3
  import { chainIdToPlatform } from "./platform";
5
4
  import {
6
5
  ExternalPlatform,
@@ -10,6 +9,17 @@ import {
10
9
  WalletAdapter,
11
10
  } from "./types";
12
11
 
12
+ // Shared store across all EthereumWalletBase instances so late EIP-6963
13
+ // announcements are captured once and visible to every wallet adapter.
14
+ let sharedStore: Store | undefined;
15
+
16
+ function getSharedStore(): Store {
17
+ if (!sharedStore) {
18
+ sharedStore = createStore();
19
+ }
20
+ return sharedStore;
21
+ }
22
+
13
23
  export abstract class EthereumWalletBase implements WalletAdapter {
14
24
  abstract readonly type: ExternalWalletType;
15
25
  abstract readonly rdns: string;
@@ -17,7 +27,6 @@ export abstract class EthereumWalletBase implements WalletAdapter {
17
27
 
18
28
  platform: ExternalPlatform | undefined;
19
29
  protected account: string | undefined = undefined;
20
- protected store = createStore();
21
30
  protected provider: EIP6963ProviderDetail | undefined;
22
31
  protected connectedAccounts: string[] = [];
23
32
 
@@ -26,10 +35,10 @@ export abstract class EthereumWalletBase implements WalletAdapter {
26
35
  }
27
36
 
28
37
  private getProvider(): EIP6963ProviderDetail | undefined {
29
- if (!this.provider) {
30
- this.provider = this.store
31
- .getProviders()
32
- .find((provider) => provider.info.rdns === this.rdns);
38
+ // Use shared store's findProvider which reflects late announcements
39
+ const found = getSharedStore().findProvider({ rdns: this.rdns as any });
40
+ if (found) {
41
+ this.provider = found;
33
42
  }
34
43
  return this.provider;
35
44
  }
@@ -40,15 +49,14 @@ export abstract class EthereumWalletBase implements WalletAdapter {
40
49
  return provider.provider;
41
50
  }
42
51
 
43
- // Fallback for MetaMask when not announced via EIP-6963
44
- if (
45
- this.rdns === "io.metamask" &&
46
- typeof window !== "undefined" &&
47
- (window as any).ethereum?.isMetaMask
48
- ) {
49
- return (window as any).ethereum;
50
- }
52
+ return this.getFallbackProvider();
53
+ }
51
54
 
55
+ /**
56
+ * Fallback provider detection when EIP-6963 announcement is missed.
57
+ * Subclasses can override to provide wallet-specific fallback logic.
58
+ */
59
+ protected getFallbackProvider(): any {
52
60
  return null;
53
61
  }
54
62
 
@@ -101,29 +109,20 @@ export abstract class EthereumWalletBase implements WalletAdapter {
101
109
  }
102
110
 
103
111
  isAvailable(): boolean {
104
- if (isMobile()) {
105
- return false;
106
- }
107
-
108
112
  // Check dynamically each time, as the provider might be announced after instantiation
109
113
  const provider = this.getProvider();
110
114
 
111
- // Also check for MetaMask via window.ethereum as a fallback for MetaMask specifically
112
- if (
113
- !provider &&
114
- this.rdns === "io.metamask" &&
115
- typeof window !== "undefined"
116
- ) {
117
- // MetaMask might be available via window.ethereum even if not announced via EIP-6963 yet
118
- return !!(window as any).ethereum?.isMetaMask;
119
- }
120
-
121
115
  // Initialize if we just found the provider
122
116
  if (provider && !this.initialized) {
123
117
  this.initializeIfAvailable();
124
118
  }
125
119
 
126
- return typeof window !== "undefined" && !!provider;
120
+ if (provider) {
121
+ return true;
122
+ }
123
+
124
+ // Fall back to wallet-specific detection when EIP-6963 announcement is missed
125
+ return typeof window !== "undefined" && !!this.getFallbackProvider();
127
126
  }
128
127
 
129
128
  getInfo(): ExternalWallet {
@@ -158,18 +157,7 @@ export abstract class EthereumWalletBase implements WalletAdapter {
158
157
  throw new Error(`${this.displayName} is not available`);
159
158
  }
160
159
 
161
- let ethereum: any;
162
- const provider = this.getProvider();
163
-
164
- if (provider) {
165
- ethereum = provider.provider;
166
- } else if (
167
- this.rdns === "io.metamask" &&
168
- (window as any).ethereum?.isMetaMask
169
- ) {
170
- // Fallback for MetaMask when not announced via EIP-6963
171
- ethereum = (window as any).ethereum;
172
- }
160
+ const ethereum = this.getEthereumProvider();
173
161
 
174
162
  if (!ethereum) {
175
163
  throw new Error(`${this.displayName} provider not found`);
@@ -183,15 +171,14 @@ export abstract class EthereumWalletBase implements WalletAdapter {
183
171
  this.account = getAddress(accounts[0]);
184
172
  this.connectedAccounts = accounts.map(getAddress);
185
173
 
186
- // If we used the fallback, store the ethereum provider for future use
187
- if (!provider && this.rdns === "io.metamask") {
188
- // Create a mock EIP6963ProviderDetail for consistency
174
+ // If we used a fallback provider, store it for future use
175
+ if (!this.getProvider()) {
189
176
  this.provider = {
190
177
  info: {
191
- uuid: "metamask-fallback",
192
- name: "MetaMask",
178
+ uuid: `${this.rdns}-fallback`,
179
+ name: this.displayName,
193
180
  icon: "data:image/svg+xml;base64,",
194
- rdns: "io.metamask",
181
+ rdns: this.rdns,
195
182
  },
196
183
  provider: ethereum,
197
184
  } as EIP6963ProviderDetail;
@@ -5,4 +5,10 @@ export class MetaMaskWallet extends EthereumWalletBase {
5
5
  readonly type: ExternalWalletType = "metamask";
6
6
  readonly rdns = "io.metamask";
7
7
  readonly displayName = "MetaMask";
8
+
9
+ protected getFallbackProvider(): any {
10
+ return (window as any).ethereum?.isMetaMask
11
+ ? (window as any).ethereum
12
+ : null;
13
+ }
8
14
  }
@@ -5,4 +5,8 @@ export class PhantomEVMWallet extends EthereumWalletBase {
5
5
  readonly type: ExternalWalletType = "phantom-evm";
6
6
  readonly rdns = "app.phantom";
7
7
  readonly displayName = "Phantom";
8
+
9
+ protected getFallbackProvider(): any {
10
+ return (window as any).phantom?.ethereum ?? null;
11
+ }
8
12
  }