Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { SpendingLimitsProvider } from "@phantom/embedded-provider-core";

export class BrowserSpendingLimitsProvider implements SpendingLimitsProvider {
upsertSpendingLimit(_args: unknown): Promise<unknown> {
return Promise.resolve();
}
}
12 changes: 10 additions & 2 deletions packages/browser-sdk/src/providers/embedded/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand Down
20 changes: 14 additions & 6 deletions packages/embedded-provider-core/src/auth-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
AuthResult,
EmbeddedStorage,
AuthProvider,
SpendingLimitsProvider,
URLParamsAccessor,
} from "./interfaces";
import type { StamperWithKeyManagement } from "@phantom/sdk-types";
Expand All @@ -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 }),
}));
Expand Down Expand Up @@ -87,6 +88,7 @@ describe("EmbeddedProvider Auth Flows", () => {
let mockLogger: DebugLogger;
let mockStorage: jest.Mocked<EmbeddedStorage>;
let mockAuthProvider: jest.Mocked<AuthProvider>;
let mockSpendingLimitsProvider: jest.Mocked<SpendingLimitsProvider>;
let mockURLParamsAccessor: jest.Mocked<URLParamsAccessor>;
let mockStamper: jest.Mocked<StamperWithKeyManagement>;
let mockClient: jest.Mocked<PhantomClient>;
Expand Down Expand Up @@ -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),
Expand All @@ -151,6 +158,7 @@ describe("EmbeddedProvider Auth Flows", () => {
name: "test-platform",
storage: mockStorage,
authProvider: mockAuthProvider,
spendingLimitsProvider: mockSpendingLimitsProvider,
urlParamsAccessor: mockURLParamsAccessor,
stamper: mockStamper,
};
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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 () => {
Expand Down
17 changes: 12 additions & 5 deletions packages/embedded-provider-core/src/embedded-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { EmbeddedEthereumChain, EmbeddedSolanaChain } from "./chains";
import type {
AuthProvider,
AuthResult,
SpendingLimitsProvider,
DebugLogger,
EmbeddedStorage,
PlatformAdapter,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -891,6 +894,14 @@ export class EmbeddedProvider {
return await parseTransactionResponse(rawResponse.rawTransaction, params.networkId, rawResponse.hash);
}

async upsertSpendingLimit(_args: unknown): Promise<unknown> {
if (!this.client || !this.walletId) {
throw new Error("Not connected");
}

return await this.spendingLimitsProvider.upsertSpendingLimit(_args);
}

getAddresses(): WalletAddress[] {
return this.addresses;
}
Expand Down Expand Up @@ -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<Session> {
private async handlePhantomAuth(publicKey: string, stamperInfo: StamperInfo, expiresInMs: number): Promise<Session> {
this.logger.info("EMBEDDED_PROVIDER", "Starting Phantom authentication flow");

// Check if Phantom app is available (extension or mobile)
Expand Down
1 change: 1 addition & 0 deletions packages/embedded-provider-core/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./storage";
export * from "./url-params";
export * from "./auth";
export * from "./spending-limits";
export * from "./platform";
4 changes: 3 additions & 1 deletion packages/embedded-provider-core/src/interfaces/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -11,6 +12,7 @@ export interface PlatformAdapter {
storage: EmbeddedStorage;
authProvider: AuthProvider;
phantomAppProvider: PhantomAppProvider;
spendingLimitsProvider: SpendingLimitsProvider;
urlParamsAccessor: URLParamsAccessor;
stamper: StamperWithKeyManagement;
analyticsHeaders?: Partial<ClientSideSdkHeaders>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface SpendingLimitsProvider {
upsertSpendingLimit(args: unknown): Promise<unknown>;
}
8 changes: 7 additions & 1 deletion packages/embedded-provider-core/src/renewal.test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -26,6 +26,7 @@ describe.skip("EmbeddedProvider Renewal Tests", () => {
let provider: EmbeddedProvider;
let mockStamper: jest.Mocked<StamperWithKeyManagement>;
let mockClient: jest.Mocked<PhantomClient>;
let mockSpendingLimitsProvider: jest.Mocked<SpendingLimitsProvider>;
let mockStorage: { [key: string]: any };
let originalDate: typeof Date;

Expand Down Expand Up @@ -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",
Expand Down
15 changes: 10 additions & 5 deletions packages/react-native-sdk/src/PhantomProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
},
Expand All @@ -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({
Expand All @@ -76,6 +82,7 @@ export function PhantomProvider({ children, config, debugConfig }: PhantomProvid
const platform: PlatformAdapter = {
storage,
authProvider,
spendingLimitsProvider,
urlParamsAccessor,
stamper,
phantomAppProvider: new ReactNativePhantomAppProvider(),
Expand All @@ -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);
Expand Down Expand Up @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { SpendingLimitsProvider } from "@phantom/embedded-provider-core";

export class ExpoSpendingLimitsProvider implements SpendingLimitsProvider {
upsertSpendingLimit(_args: unknown): Promise<unknown> {
return Promise.resolve();
}
}
Loading