diff --git a/packages/browser-sdk/src/providers/embedded/adapters/index.ts b/packages/browser-sdk/src/providers/embedded/adapters/index.ts index f6179386..c146a435 100644 --- a/packages/browser-sdk/src/providers/embedded/adapters/index.ts +++ b/packages/browser-sdk/src/providers/embedded/adapters/index.ts @@ -2,4 +2,5 @@ export * from "./storage"; export * from "./url-params"; export * from "./auth"; export * from "./phantom-app"; +export * from "./spending-limits"; export * from "./logger"; diff --git a/packages/browser-sdk/src/providers/embedded/adapters/spending-limits.ts b/packages/browser-sdk/src/providers/embedded/adapters/spending-limits.ts new file mode 100644 index 00000000..87530bd2 --- /dev/null +++ b/packages/browser-sdk/src/providers/embedded/adapters/spending-limits.ts @@ -0,0 +1,7 @@ +import type { SpendingLimitsProvider } from "@phantom/embedded-provider-core"; + +export class BrowserSpendingLimitsProvider implements SpendingLimitsProvider { + upsertSpendingLimit(_args: unknown): Promise { + return Promise.resolve(); + } +} diff --git a/packages/browser-sdk/src/providers/embedded/index.ts b/packages/browser-sdk/src/providers/embedded/index.ts index 3d34833b..2ec252d2 100644 --- a/packages/browser-sdk/src/providers/embedded/index.ts +++ b/packages/browser-sdk/src/providers/embedded/index.ts @@ -1,7 +1,14 @@ import { EmbeddedProvider as CoreEmbeddedProvider } from "@phantom/embedded-provider-core"; import type { EmbeddedProviderConfig, PlatformAdapter } from "@phantom/embedded-provider-core"; import { IndexedDbStamper } from "@phantom/indexed-db-stamper"; -import { BrowserStorage, BrowserURLParamsAccessor, BrowserAuthProvider, BrowserPhantomAppProvider, BrowserLogger } from "./adapters"; +import { + BrowserStorage, + BrowserURLParamsAccessor, + BrowserAuthProvider, + BrowserPhantomAppProvider, + BrowserLogger, + BrowserSpendingLimitsProvider, +} from "./adapters"; import { debug, DebugCategory } from "../../debug"; import { detectBrowser, getPlatformName } from "../../utils/browser-detection"; import type { Provider } from "../../types"; @@ -20,12 +27,13 @@ export class EmbeddedProvider extends CoreEmbeddedProvider implements Provider { }); const platformName = getPlatformName(); - const { name: browserName, version} = detectBrowser(); + const { name: browserName, version } = detectBrowser(); const platform: PlatformAdapter = { storage: new BrowserStorage(), authProvider: new BrowserAuthProvider(urlParamsAccessor), phantomAppProvider: new BrowserPhantomAppProvider(), + spendingLimitsProvider: new BrowserSpendingLimitsProvider(), urlParamsAccessor, stamper, name: platformName, // Use detected browser name and version for identification diff --git a/packages/embedded-provider-core/src/auth-flow.test.ts b/packages/embedded-provider-core/src/auth-flow.test.ts index 52889c31..b93b77c2 100644 --- a/packages/embedded-provider-core/src/auth-flow.test.ts +++ b/packages/embedded-provider-core/src/auth-flow.test.ts @@ -7,6 +7,7 @@ import type { AuthResult, EmbeddedStorage, AuthProvider, + SpendingLimitsProvider, URLParamsAccessor, } from "./interfaces"; import type { StamperWithKeyManagement } from "@phantom/sdk-types"; @@ -20,10 +21,10 @@ jest.mock("@phantom/parsers", () => ({ parseMessage: jest.fn().mockReturnValue({ base64url: "mock-base64url" }), parseTransactionToBase64Url: jest.fn().mockResolvedValue({ base64url: "mock-base64url", originalFormat: "mock" }), parseSignMessageResponse: jest.fn().mockReturnValue({ signature: "mock-signature", rawSignature: "mock-raw" }), - parseTransactionResponse: jest.fn().mockReturnValue({ - hash: "mock-transaction-hash", + parseTransactionResponse: jest.fn().mockReturnValue({ + hash: "mock-transaction-hash", rawTransaction: "mock-raw-tx", - blockExplorer: "https://explorer.com/tx/mock-transaction-hash" + blockExplorer: "https://explorer.com/tx/mock-transaction-hash", }), parseSolanaTransactionSignature: jest.fn().mockReturnValue({ signature: "mock-signature", fallback: false }), })); @@ -87,6 +88,7 @@ describe("EmbeddedProvider Auth Flows", () => { let mockLogger: DebugLogger; let mockStorage: jest.Mocked; let mockAuthProvider: jest.Mocked; + let mockSpendingLimitsProvider: jest.Mocked; let mockURLParamsAccessor: jest.Mocked; let mockStamper: jest.Mocked; let mockClient: jest.Mocked; @@ -129,6 +131,11 @@ describe("EmbeddedProvider Auth Flows", () => { resumeAuthFromRedirect: jest.fn(), }; + // Mock spending limits provider + mockSpendingLimitsProvider = { + upsertSpendingLimit: jest.fn(), + }; + // Mock URL params accessor mockURLParamsAccessor = { getParam: jest.fn().mockReturnValue(null), @@ -151,6 +158,7 @@ describe("EmbeddedProvider Auth Flows", () => { name: "test-platform", storage: mockStorage, authProvider: mockAuthProvider, + spendingLimitsProvider: mockSpendingLimitsProvider, urlParamsAccessor: mockURLParamsAccessor, stamper: mockStamper, }; @@ -415,8 +423,6 @@ describe("EmbeddedProvider Auth Flows", () => { expect(mockAuthProvider.authenticate).toHaveBeenCalled(); }); - - it("should fall back to fresh authentication when session is missing from database but URL has session_id", async () => { // Setup: URL contains session_id parameter (session was wiped from DB) mockURLParamsAccessor.getParam.mockReturnValue("wiped-session-123"); @@ -865,7 +871,9 @@ describe("EmbeddedProvider Auth Flows", () => { mockStorage.getSession.mockResolvedValue(null); mockAuthProvider.authenticate.mockRejectedValue(new Error("IndexedDB access denied")); - await expect(provider.connect()).rejects.toThrow("Storage error: Unable to access browser storage. Please ensure storage is available and try again."); + await expect(provider.connect()).rejects.toThrow( + "Storage error: Unable to access browser storage. Please ensure storage is available and try again.", + ); }); it("should clean up state on authentication failures", async () => { diff --git a/packages/embedded-provider-core/src/embedded-provider.ts b/packages/embedded-provider-core/src/embedded-provider.ts index 8c280f22..6d18013c 100644 --- a/packages/embedded-provider-core/src/embedded-provider.ts +++ b/packages/embedded-provider-core/src/embedded-provider.ts @@ -20,6 +20,7 @@ import { EmbeddedEthereumChain, EmbeddedSolanaChain } from "./chains"; import type { AuthProvider, AuthResult, + SpendingLimitsProvider, DebugLogger, EmbeddedStorage, PlatformAdapter, @@ -86,6 +87,7 @@ export class EmbeddedProvider { private authProvider: AuthProvider; // Phantom App (mobile and extension provider) deeplinks to our wallet for phantom connect private phantomAppProvider: PhantomAppProvider; + private spendingLimitsProvider: SpendingLimitsProvider; private urlParamsAccessor: URLParamsAccessor; private stamper: StamperWithKeyManagement; private logger: DebugLogger; @@ -113,6 +115,7 @@ export class EmbeddedProvider { this.storage = platform.storage; this.authProvider = platform.authProvider; this.phantomAppProvider = platform.phantomAppProvider; + this.spendingLimitsProvider = platform.spendingLimitsProvider; this.urlParamsAccessor = platform.urlParamsAccessor; this.stamper = platform.stamper; this.jwtAuth = new JWTAuth(); @@ -891,6 +894,14 @@ export class EmbeddedProvider { return await parseTransactionResponse(rawResponse.rawTransaction, params.networkId, rawResponse.hash); } + async upsertSpendingLimit(_args: unknown): Promise { + if (!this.client || !this.walletId) { + throw new Error("Not connected"); + } + + return await this.spendingLimitsProvider.upsertSpendingLimit(_args); + } + getAddresses(): WalletAddress[] { return this.addresses; } @@ -1048,11 +1059,7 @@ export class EmbeddedProvider { * 4. Start a polling mechanism to check for auth completion * 5. Update the session when the mobile app completes the auth */ - private async handlePhantomAuth( - publicKey: string, - stamperInfo: StamperInfo, - expiresInMs: number, - ): Promise { + private async handlePhantomAuth(publicKey: string, stamperInfo: StamperInfo, expiresInMs: number): Promise { this.logger.info("EMBEDDED_PROVIDER", "Starting Phantom authentication flow"); // Check if Phantom app is available (extension or mobile) diff --git a/packages/embedded-provider-core/src/interfaces/index.ts b/packages/embedded-provider-core/src/interfaces/index.ts index a6ed1ae5..a9cc5779 100644 --- a/packages/embedded-provider-core/src/interfaces/index.ts +++ b/packages/embedded-provider-core/src/interfaces/index.ts @@ -1,4 +1,5 @@ export * from "./storage"; export * from "./url-params"; export * from "./auth"; +export * from "./spending-limits"; export * from "./platform"; diff --git a/packages/embedded-provider-core/src/interfaces/platform.ts b/packages/embedded-provider-core/src/interfaces/platform.ts index fdef5d2e..e6356eae 100644 --- a/packages/embedded-provider-core/src/interfaces/platform.ts +++ b/packages/embedded-provider-core/src/interfaces/platform.ts @@ -2,7 +2,8 @@ import type { EmbeddedStorage } from "./storage"; import type { AuthProvider, PhantomAppProvider } from "./auth"; import type { URLParamsAccessor } from "./url-params"; import type { StamperWithKeyManagement } from "@phantom/sdk-types"; -import type { ClientSideSdkHeaders } from "@phantom/constants"; +import type { ClientSideSdkHeaders } from "@phantom/constants"; +import type { SpendingLimitsProvider } from "./spending-limits"; export interface PlatformAdapter { name: string; // Platform identifier like "web", "ios", "android", "react-native", etc. @@ -11,6 +12,7 @@ export interface PlatformAdapter { storage: EmbeddedStorage; authProvider: AuthProvider; phantomAppProvider: PhantomAppProvider; + spendingLimitsProvider: SpendingLimitsProvider; urlParamsAccessor: URLParamsAccessor; stamper: StamperWithKeyManagement; analyticsHeaders?: Partial; diff --git a/packages/embedded-provider-core/src/interfaces/spending-limits.ts b/packages/embedded-provider-core/src/interfaces/spending-limits.ts new file mode 100644 index 00000000..20d05ce3 --- /dev/null +++ b/packages/embedded-provider-core/src/interfaces/spending-limits.ts @@ -0,0 +1,3 @@ +export interface SpendingLimitsProvider { + upsertSpendingLimit(args: unknown): Promise; +} diff --git a/packages/embedded-provider-core/src/renewal.test.ts b/packages/embedded-provider-core/src/renewal.test.ts index 7dd9953b..9386c6f8 100644 --- a/packages/embedded-provider-core/src/renewal.test.ts +++ b/packages/embedded-provider-core/src/renewal.test.ts @@ -1,5 +1,5 @@ import { EmbeddedProvider } from "./embedded-provider"; -import type { EmbeddedProviderConfig, PlatformAdapter, Session } from "./interfaces"; +import type { EmbeddedProviderConfig, PlatformAdapter, Session, SpendingLimitsProvider } from "./interfaces"; import type { StamperWithKeyManagement } from "@phantom/sdk-types"; import type { PhantomClient } from "@phantom/client"; @@ -26,6 +26,7 @@ describe.skip("EmbeddedProvider Renewal Tests", () => { let provider: EmbeddedProvider; let mockStamper: jest.Mocked; let mockClient: jest.Mocked; + let mockSpendingLimitsProvider: jest.Mocked; let mockStorage: { [key: string]: any }; let originalDate: typeof Date; @@ -85,10 +86,15 @@ describe.skip("EmbeddedProvider Renewal Tests", () => { }), } as any; + mockSpendingLimitsProvider = { + upsertSpendingLimit: jest.fn(), + }; + // Mock platform adapter const mockPlatform: PlatformAdapter = { storage: mockEmbeddedStorage, authProvider: {} as any, + spendingLimitsProvider: mockSpendingLimitsProvider, urlParamsAccessor: {} as any, stamper: mockStamper, name: "test-platform", diff --git a/packages/react-native-sdk/src/PhantomProvider.tsx b/packages/react-native-sdk/src/PhantomProvider.tsx index b44e871f..f1e64026 100644 --- a/packages/react-native-sdk/src/PhantomProvider.tsx +++ b/packages/react-native-sdk/src/PhantomProvider.tsx @@ -3,10 +3,16 @@ import { createContext, useContext, useState, useEffect, useMemo } from "react"; import { EmbeddedProvider } from "@phantom/embedded-provider-core"; import type { EmbeddedProviderConfig, PlatformAdapter, ConnectEventData, ConnectResult } from "@phantom/embedded-provider-core"; import type { PhantomSDKConfig, PhantomDebugConfig, WalletAddress } from "./types"; -import {ANALYTICS_HEADERS, DEFAULT_WALLET_API_URL, DEFAULT_EMBEDDED_WALLET_TYPE, DEFAULT_AUTH_URL } from "@phantom/constants"; +import { + ANALYTICS_HEADERS, + DEFAULT_WALLET_API_URL, + DEFAULT_EMBEDDED_WALLET_TYPE, + DEFAULT_AUTH_URL, +} from "@phantom/constants"; // Platform adapters for React Native/Expo import { ExpoSecureStorage } from "./providers/embedded/storage"; import { ExpoAuthProvider } from "./providers/embedded/auth"; +import { ExpoSpendingLimitsProvider } from "./providers/embedded/spending-limits"; import { ExpoURLParamsAccessor } from "./providers/embedded/url-params"; import { ReactNativeStamper } from "./providers/embedded/stamper"; import { ExpoLogger } from "./providers/embedded/logger"; @@ -51,8 +57,7 @@ export function PhantomProvider({ children, config, debugConfig }: PhantomProvid apiBaseUrl: config.apiBaseUrl || DEFAULT_WALLET_API_URL, embeddedWalletType: config.embeddedWalletType || DEFAULT_EMBEDDED_WALLET_TYPE, authOptions: { - ...(config.authOptions || { - }), + ...(config.authOptions || {}), redirectUrl, authUrl: config.authOptions?.authUrl || DEFAULT_AUTH_URL, }, @@ -64,6 +69,7 @@ export function PhantomProvider({ children, config, debugConfig }: PhantomProvid // Create platform adapters const storage = new ExpoSecureStorage(); const authProvider = new ExpoAuthProvider(); + const spendingLimitsProvider = new ExpoSpendingLimitsProvider(); const urlParamsAccessor = new ExpoURLParamsAccessor(); const logger = new ExpoLogger(debugConfig?.enabled || false); const stamper = new ReactNativeStamper({ @@ -76,6 +82,7 @@ export function PhantomProvider({ children, config, debugConfig }: PhantomProvid const platform: PlatformAdapter = { storage, authProvider, + spendingLimitsProvider, urlParamsAccessor, stamper, phantomAppProvider: new ReactNativePhantomAppProvider(), @@ -95,7 +102,6 @@ export function PhantomProvider({ children, config, debugConfig }: PhantomProvid // Event listener management - SDK already exists useEffect(() => { - // Event handlers that need to be referenced for cleanup const handleConnectStart = () => { setIsConnecting(true); @@ -156,7 +162,6 @@ export function PhantomProvider({ children, config, debugConfig }: PhantomProvid // Initialize auto-connect useEffect(() => { - // Attempt auto-connect if enabled if (config.autoConnect !== false) { sdk.autoConnect().catch(() => { diff --git a/packages/react-native-sdk/src/providers/embedded/spending-limits.ts b/packages/react-native-sdk/src/providers/embedded/spending-limits.ts new file mode 100644 index 00000000..ed16372a --- /dev/null +++ b/packages/react-native-sdk/src/providers/embedded/spending-limits.ts @@ -0,0 +1,7 @@ +import type { SpendingLimitsProvider } from "@phantom/embedded-provider-core"; + +export class ExpoSpendingLimitsProvider implements SpendingLimitsProvider { + upsertSpendingLimit(_args: unknown): Promise { + return Promise.resolve(); + } +}