Skip to content

Latest commit

 

History

History
656 lines (521 loc) · 16.5 KB

File metadata and controls

656 lines (521 loc) · 16.5 KB

@proton/web-sdk Frontend Integration

This guide covers wallet connection and transaction signing for web applications using @proton/web-sdk.

Installation

npm install @proton/web-sdk @proton/link
# or
yarn add @proton/web-sdk @proton/link

Important: The @proton/link package is required for mobile wallet support. See Mobile Wallet Support for details.

Quick Start

import ProtonWebSDK from '@proton/web-sdk';
import '@proton/link'; // Required for mobile wallet support

// Login
const { link, session } = await ProtonWebSDK({
  linkOptions: {
    chainId: '384da888112027f0321850a169f737c33e53b388aad48b5adace4bab97f437e0',
    endpoints: ['https://proton.eosusa.io']
  },
  selectorOptions: { appName: 'My dApp' }
});

// session.auth = { actor: 'username', permission: 'active' }
console.log('Logged in as:', session.auth.actor);

// Send transaction
const result = await session.transact({
  actions: [{
    account: 'eosio.token',
    name: 'transfer',
    authorization: [session.auth],
    data: {
      from: session.auth.actor,
      to: 'recipient',
      quantity: '1.0000 XPR',
      memo: 'Hello!'
    }
  }]
}, { broadcast: true });

ProtonWebSDK Options

linkOptions (required)

Key Type Required Description
endpoints string[] Yes Array of RPC endpoints (multiple for fault tolerance)
chainId string No Chain ID (defaults to mainnet)
storage LinkStorage No Custom storage adapter
storagePrefix string No Prefix for storage keys (default: proton-storage)
restoreSession boolean No Restore previous session without wallet selector

transportOptions (required for mobile)

Key Type Description
requestAccount string Required for mobile. Your dApp's account name - used for deep link callbacks so mobile app knows where to return after signing
backButton boolean Show back button in modal (default: true)

Important: Without requestAccount, the WebAuth mobile app will sign transactions but won't return to your browser. Always set this to your contract or dApp account name.

selectorOptions (optional)

Key Type Description
appName string Your app name (shown in wallet selector)
appLogo string URL to your app logo
enabledWalletTypes string[] Wallet types to show: proton, webauth, anchor
customStyleOptions object Custom styling for modal

Complete Service Class Example

import ProtonWebSDK from '@proton/web-sdk';

const CHAIN_ID = '384da888112027f0321850a169f737c33e53b388aad48b5adace4bab97f437e0';
const ENDPOINTS = ['https://proton.eosusa.io', 'https://proton.protonuk.io'];

class ProtonService {
  private link: any = null;
  private session: any = null;

  get isLoggedIn(): boolean {
    return this.session !== null;
  }

  get actor(): string {
    return this.session?.auth?.actor ?? '';
  }

  get permission(): string {
    return this.session?.auth?.permission ?? 'active';
  }

  async login(): Promise<{ actor: string; permission: string } | null> {
    try {
      const { link, session } = await ProtonWebSDK({
        linkOptions: {
          chainId: CHAIN_ID,
          endpoints: ENDPOINTS
        },
        transportOptions: {
          requestAccount: 'myapp'
        },
        selectorOptions: {
          appName: 'My dApp',
          appLogo: 'https://myapp.com/logo.png',
          enabledWalletTypes: ['proton', 'webauth', 'anchor']
        }
      });

      this.link = link;
      this.session = session;

      return session.auth;
    } catch (error) {
      console.error('Login failed:', error);
      return null;
    }
  }

  async restoreSession(): Promise<boolean> {
    try {
      const { link, session } = await ProtonWebSDK({
        linkOptions: {
          chainId: CHAIN_ID,
          endpoints: ENDPOINTS,
          restoreSession: true
        },
        transportOptions: {
          requestAccount: 'myapp'
        },
        selectorOptions: {
          appName: 'My dApp'
        }
      });

      if (session) {
        this.link = link;
        this.session = session;
        return true;
      }
      return false;
    } catch (error) {
      console.error('Session restore failed:', error);
      return false;
    }
  }

  async logout(): Promise<void> {
    if (this.link && this.session) {
      await this.link.removeSession('myapp', this.session.auth);
    }
    this.link = null;
    this.session = null;
  }

  async transact(actions: any[]): Promise<any> {
    if (!this.session) {
      throw new Error('Not logged in');
    }

    return this.session.transact(
      { actions },
      { broadcast: true }
    );
  }
}

export const protonService = new ProtonService();

Common Actions

Token Transfer

async function transferTokens(to: string, amount: string, memo: string = '') {
  const result = await session.transact({
    actions: [{
      account: 'eosio.token',
      name: 'transfer',
      authorization: [session.auth],
      data: {
        from: session.auth.actor,
        to: to,
        quantity: amount,  // e.g., '10.0000 XPR'
        memo: memo
      }
    }]
  }, { broadcast: true });

  return result;
}

Token Contracts

Token Contract Decimals Example
XPR eosio.token 4 1.0000 XPR
XUSDT xtokens 6 1.000000 XUSDT
FOOBAR xtokens 6 1.000000 FOOBAR
LOAN loan.token 4 1.0000 LOAN

Custom Contract Action

async function createBattle(amount: string, direction: number, duration: number) {
  const result = await session.transact({
    actions: [{
      account: 'pricebattle',
      name: 'create',
      authorization: [session.auth],
      data: {
        creator: session.auth.actor,
        amount: amount,
        direction: direction,  // 1=UP, 2=DOWN
        oracle_index: 4,       // BTC/USD
        duration: duration     // seconds
      }
    }]
  }, { broadcast: true });

  return result;
}

Multiple Actions in One Transaction

async function depositAndStake(amount: string) {
  const result = await session.transact({
    actions: [
      // First action: transfer
      {
        account: 'eosio.token',
        name: 'transfer',
        authorization: [session.auth],
        data: {
          from: session.auth.actor,
          to: 'stakingcontract',
          quantity: amount,
          memo: 'deposit'
        }
      },
      // Second action: stake
      {
        account: 'stakingcontract',
        name: 'stake',
        authorization: [session.auth],
        data: {
          account: session.auth.actor,
          amount: amount
        }
      }
    ]
  }, { broadcast: true });

  return result;
}

Custom Styling

const { link, session } = await ProtonWebSDK({
  linkOptions: { /* ... */ },
  selectorOptions: {
    appName: 'My dApp',
    customStyleOptions: {
      modalBackgroundColor: '#1a1a2e',
      logoBackgroundColor: '#16213e',
      isLogoRound: true,
      optionBackgroundColor: '#0f3460',
      optionFontColor: '#e94560',
      primaryFontColor: '#ffffff',
      secondaryFontColor: '#a0a0a0',
      linkColor: '#e94560'
    }
  }
});

Custom Storage

By default, the SDK uses localStorage. For custom storage (e.g., encrypted storage, server-side):

interface LinkStorage {
  write(key: string, data: string): Promise<void>;
  read(key: string): Promise<string | null>;
  remove(key: string): Promise<void>;
}

class SecureStorage implements LinkStorage {
  async write(key: string, data: string): Promise<void> {
    // Your secure storage logic
    localStorage.setItem(key, encrypt(data));
  }

  async read(key: string): Promise<string | null> {
    const data = localStorage.getItem(key);
    return data ? decrypt(data) : null;
  }

  async remove(key: string): Promise<void> {
    localStorage.removeItem(key);
  }
}

const { link, session } = await ProtonWebSDK({
  linkOptions: {
    endpoints: ENDPOINTS,
    storage: new SecureStorage()
  },
  // ...
});

React Integration

Context Provider

import React, { createContext, useContext, useState, useEffect } from 'react';
import ProtonWebSDK from '@proton/web-sdk';

interface ProtonContextType {
  session: any;
  login: () => Promise<void>;
  logout: () => Promise<void>;
  transact: (actions: any[]) => Promise<any>;
}

const ProtonContext = createContext<ProtonContextType | null>(null);

export function ProtonProvider({ children }: { children: React.ReactNode }) {
  const [link, setLink] = useState<any>(null);
  const [session, setSession] = useState<any>(null);

  useEffect(() => {
    // Restore session on mount
    restoreSession();
  }, []);

  async function restoreSession() {
    try {
      const { link, session } = await ProtonWebSDK({
        linkOptions: {
          chainId: CHAIN_ID,
          endpoints: ENDPOINTS,
          restoreSession: true
        },
        selectorOptions: { appName: 'My dApp' }
      });

      if (session) {
        setLink(link);
        setSession(session);
      }
    } catch (e) {
      console.error('Restore failed:', e);
    }
  }

  async function login() {
    const { link, session } = await ProtonWebSDK({
      linkOptions: {
        chainId: CHAIN_ID,
        endpoints: ENDPOINTS
      },
      selectorOptions: { appName: 'My dApp' }
    });

    setLink(link);
    setSession(session);
  }

  async function logout() {
    if (link && session) {
      await link.removeSession('myapp', session.auth);
    }
    setLink(null);
    setSession(null);
  }

  async function transact(actions: any[]) {
    if (!session) throw new Error('Not logged in');
    return session.transact({ actions }, { broadcast: true });
  }

  return (
    <ProtonContext.Provider value={{ session, login, logout, transact }}>
      {children}
    </ProtonContext.Provider>
  );
}

export function useProton() {
  const context = useContext(ProtonContext);
  if (!context) throw new Error('useProton must be used within ProtonProvider');
  return context;
}

Usage in Components

function WalletButton() {
  const { session, login, logout } = useProton();

  if (session) {
    return (
      <div>
        <span>Connected: {session.auth.actor}</span>
        <button onClick={logout}>Disconnect</button>
      </div>
    );
  }

  return <button onClick={login}>Connect Wallet</button>;
}

Error Handling

async function safeTransact(actions: any[]) {
  try {
    const result = await session.transact({ actions }, { broadcast: true });
    return { success: true, result };
  } catch (error: any) {
    // User cancelled
    if (error.message?.includes('User cancelled')) {
      return { success: false, error: 'Transaction cancelled by user' };
    }

    // Insufficient resources
    if (error.message?.includes('insufficient')) {
      return { success: false, error: 'Insufficient resources (RAM/CPU/NET)' };
    }

    // Contract assertion failed
    if (error.message?.includes('assertion failure')) {
      const match = error.message.match(/assertion failure with message: (.+)/);
      return { success: false, error: match?.[1] ?? 'Transaction failed' };
    }

    return { success: false, error: error.message ?? 'Unknown error' };
  }
}

Mobile Wallet Support

For mobile wallet signing to work (WebAuth iOS/Android app), you must install and import @proton/link.

Why @proton/link is Required

The @proton/link package provides the transport layer for mobile deep linking. Without it, the SDK cannot communicate with the WebAuth mobile app to request transaction signatures.

Installation

npm install @proton/web-sdk @proton/link

Import Pattern

import ProtonWebSDK from '@proton/web-sdk';
import '@proton/link'; // Required - enables mobile deep linking transport

Note: The @proton/link import doesn't expose any API you need to call directly. Simply importing it registers the transport handlers needed for mobile wallet communication.

Dynamic Import Pattern (Required for Mobile)

Important: Static imports often fail for mobile wallet support. Use dynamic imports with Promise.all to ensure @proton/link is fully loaded before any wallet operations:

let ConnectWallet: any;
let sdkReady: Promise<void> | null = null;

if (typeof window !== 'undefined') {
  sdkReady = Promise.all([
    import('@proton/web-sdk').then((mod) => {
      ConnectWallet = mod.default;
    }),
    import('@proton/link')  // Critical for mobile deep linking
  ]).then(() => {});
}

// Helper to ensure SDK is loaded before use
const waitForSdk = async () => {
  if (sdkReady) await sdkReady;
};

// Always await before using ConnectWallet
async function login() {
  await waitForSdk();
  const { link, session } = await ConnectWallet({
    // ... options
  });
}

This pattern ensures both packages are fully loaded and transport handlers are registered before any wallet connection attempts.

Critical Settings Checklist for Mobile

For mobile wallet to work correctly, ensure ALL of these:

  1. Install both packages: npm install @proton/web-sdk @proton/link

  2. Use dynamic imports with Promise.all (not static imports):

    sdkReady = Promise.all([
      import('@proton/web-sdk').then((mod) => { ConnectWallet = mod.default; }),
      import('@proton/link')
    ]).then(() => {});
  3. Set requestAccount to your dApp/contract name:

    transportOptions: {
      requestAccount: 'mycontract',  // Required for mobile callback
    }
  4. Enable wallet types explicitly:

    selectorOptions: {
      appName: 'My dApp',
      enabledWalletTypes: ['webauth', 'proton'],
    }

Symptoms and Causes

Symptom Cause Fix
Mobile stuck on "Processing..." Missing @proton/link or static import Use dynamic import pattern
App signs but doesn't return to browser requestAccount empty or missing Set to your contract name
Only browser wallet shown enabledWalletTypes missing proton Add ['webauth', 'proton']
"Unknown requestor" error Missing @proton/link Install and import the package

Safari iOS Popup Blocker

Safari iOS blocks popups by default, which prevents the WebAuth browser wallet from opening. Users on Safari iOS should either:

  1. Disable popup blocker: Settings > Safari > Block Pop-ups OFF
  2. Use the WebAuth mobile app instead of the browser wallet (recommended)
  3. Use a different browser (Chrome, Firefox)

Developer tip: Show a help message after several seconds of "processing" to guide users to check their popup blocker settings or switch to the WebAuth mobile app.

Recommended Versions

  • @proton/web-sdk@^4.4.1 or later
  • @proton/link@^3.2.3-27 or later

Known Issues

WebAuth iOS App Session Caching

Status: Unresolved - issue is in WebAuth iOS app

Symptom: After logout + login with different account via WebAuth iOS, the UI shows new account but transactions fail with wrong signature.

Workaround:

  • Force quit WebAuth iOS app before switching accounts
  • Or use webauth.com web wallet (works correctly)

Session Not Restoring

If restoreSession: true doesn't restore the session:

  • Clear localStorage keys starting with proton-storage or your custom prefix
  • Ensure you're on the same domain where the session was created
  • Check if storage is being blocked (private browsing, etc.)

Next.js Considerations

For SSR frameworks, the SDK must only run client-side:

// Only import client-side
let ProtonWebSDK: any;
if (typeof window !== 'undefined') {
  ProtonWebSDK = require('@proton/web-sdk').default;
}

// Check before using
async function login() {
  if (!ProtonWebSDK) {
    console.error('ProtonWebSDK not available');
    return;
  }
  // ...
}

Or use dynamic imports:

async function login() {
  const { default: ProtonWebSDK } = await import('@proton/web-sdk');
  // ...
}

Wallet Types

Type Description Use Case
proton WebAuth mobile app Primary mobile wallet
webauth webauth.com browser wallet Web-based, no app needed
anchor Anchor desktop wallet Desktop users, power users
// Show only specific wallets
selectorOptions: {
  enabledWalletTypes: ['proton', 'webauth']  // Hide Anchor
}