LearnCard icon indicating copy to clipboard operation
LearnCard copied to clipboard

feat [LC-1451]: Add partner connect SDK and example app

Open Custard7 opened this issue 3 weeks ago โ€ข 5 comments
trafficstars

Add @learncard/partner-connect SDK and Example App

Overview

This PR introduces a new Partner Connect SDK (@learncard/partner-connect) that transforms complex cross-origin postMessage communication into clean, modern Promise-based functions. It includes a fully-functional example app demonstrating all SDK capabilities.

What's New

๐Ÿ“ฆ @learncard/partner-connect SDK

A production-ready JavaScript SDK that manages the entire cross-origin message lifecycle for partner applications embedded in LearnCard.

Key Features:

  • ๐Ÿ”’ Secure: Automatic origin validation for all messages
  • ๐ŸŽฏ Type-safe: Full TypeScript support with comprehensive type definitions
  • โšก Promise-based: Modern async/await API
  • ๐Ÿงน Clean: Abstracts away all postMessage complexity
  • ๐Ÿ“ฆ Lightweight: Zero dependencies
  • ๐Ÿ›ก๏ธ Robust: Built-in timeout handling (30s default, configurable)

Location: packages/learn-card-partner-connect-sdk/

๐ŸŽจ Example App: Basic Launchpad

A complete reference implementation showcasing all SDK features in a real-world partner app scenario.

Demonstrates:

  1. SSO Authentication - requestIdentity() for user sign-on
  2. Credential Issuance - sendCredential() for issuing VCs
  3. Feature Launching - launchFeature() to navigate host app
  4. Credential Requests - askCredentialSearch() for querying user credentials
  5. Specific Credential Access - askCredentialSpecific() by ID
  6. Consent Management - requestConsent() for permissions
  7. Template Issuance - initiateTemplateIssue() for boost/template flows

Location: examples/app-store-apps/1-basic-launchpad-app/

Architecture

SDK Core Components

// 1. Initialization
const learnCard = createPartnerConnect({
  hostOrigin: 'https://learncard.app',  // Optional, defaults to learncard.app
  protocol: 'LEARNCARD_V1',              // Optional
  requestTimeout: 30000                   // Optional, in ms
});

// 2. Request Queue - Automatically managed
// Each request gets a unique ID and Promise tracking

// 3. Central Listener - Validates origin & protocol
// Resolves/rejects promises based on host responses

// 4. Public Methods - Clean, type-safe API
const identity = await learnCard.requestIdentity();
const response = await learnCard.sendCredential(credential);

Security Features

โœ… Origin Validation - All messages validated against configured hostOrigin
โœ… Protocol Verification - Messages must match expected protocol
โœ… Request ID Tracking - Only tracked requests are processed
โœ… Timeout Protection - Requests automatically timeout to prevent hanging
โœ… Explicit Target Origin - Never uses '*' as target origin

Code Comparison

Before: Manual postMessage (80+ lines of boilerplate)

const pendingRequests = new Map();

function sendPostMessage(action, payload = {}) {
  return new Promise((resolve, reject) => {
    const requestId = `${action}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
    pendingRequests.set(requestId, { resolve, reject });
    
    window.parent.postMessage({
      protocol: PROTOCOL,
      action,
      requestId,
      payload,
    }, LEARNCARD_HOST_ORIGIN);
    
    setTimeout(() => {
      if (pendingRequests.has(requestId)) {
        pendingRequests.delete(requestId);
        reject({ code: 'LC_TIMEOUT', message: 'Request timed out' });
      }
    }, 30000);
  });
}

window.addEventListener('message', (event) => {
  if (event.origin !== LEARNCARD_HOST_ORIGIN) return;
  const { protocol, requestId, type, data, error } = event.data;
  if (protocol !== PROTOCOL || !requestId) return;
  
  const pending = pendingRequests.get(requestId);
  if (!pending) return;
  
  pendingRequests.delete(requestId);
  if (type === 'SUCCESS') {
    pending.resolve(data);
  } else if (type === 'ERROR') {
    pending.reject(error);
  }
});

// Usage
const identity = await sendPostMessage('REQUEST_IDENTITY');

After: SDK (3 lines)

import { createPartnerConnect } from '@learncard/partner-connect';

const learnCard = createPartnerConnect({
  hostOrigin: 'https://learncard.app'
});

// Usage - same result, much cleaner
const identity = await learnCard.requestIdentity();

Result: 14% code reduction in example app (467 โ†’ 402 lines), massively improved maintainability

API Reference

SDK Methods

Method Description Returns
requestIdentity() Request user identity (SSO) Promise<IdentityResponse>
sendCredential(credential) Send VC to user's wallet Promise<SendCredentialResponse>
launchFeature(path, prompt?) Launch feature in host Promise<void>
askCredentialSearch(vpr) Request credentials by query Promise<CredentialSearchResponse>
askCredentialSpecific(id) Request specific credential Promise<CredentialSpecificResponse>
requestConsent(contractUri) Request user consent Promise<ConsentResponse>
initiateTemplateIssue(id, recipients?) Issue from template/boost Promise<TemplateIssueResponse>
destroy() Clean up SDK void

Error Handling

All methods reject with a LearnCardError object:

interface LearnCardError {
  code: string;  // e.g., 'LC_TIMEOUT', 'LC_UNAUTHENTICATED', 'USER_REJECTED'
  message: string;
}

Common Error Codes:

  • LC_TIMEOUT - Request timed out
  • LC_UNAUTHENTICATED - User not logged in
  • USER_REJECTED - User declined the request
  • CREDENTIAL_NOT_FOUND - Credential doesn't exist
  • UNAUTHORIZED - User lacks permission
  • TEMPLATE_NOT_FOUND - Template doesn't exist

Example App Setup

# 1. Install dependencies
pnpm install

# 2. Set up environment
cd examples/app-store-apps/1-basic-launchpad-app
cp .env.example .env
# Edit .env with your values

# 3. Run the app
pnpm --filter @learncard/app-store-demo-basic-launchpad dev

Environment Variables

# Required: Issuer seed for credential signing
LEARNCARD_ISSUER_SEED=your-hex-seed-here

# Optional: Host origin (defaults to http://localhost:3000)
LEARNCARD_HOST_ORIGIN=https://learncard.app

# Optional: Contract and boost URIs for demos
CONTRACT_URI=lc:network:network.learncard.com/trpc:contract:...
BOOST_URI=lc:network:network.learncard.com/trpc:boost:...

Usage Examples

1. SSO Authentication

try {
  const identity = await learnCard.requestIdentity();
  console.log('User DID:', identity.user.did);
  console.log('JWT Token:', identity.token);
  
  // Send token to your backend for validation
  await fetch('/api/auth', {
    method: 'POST',
    body: JSON.stringify({ token: identity.token })
  });
} catch (error) {
  if (error.code === 'LC_UNAUTHENTICATED') {
    console.log('Please log in to LearnCard');
  }
}

2. Issue Credential

const identity = await learnCard.requestIdentity();

// Your backend issues the credential
const credential = await yourBackend.issueCredential(identity.user.did);

// Send to user's wallet
const response = await learnCard.sendCredential(credential);
console.log('Credential ID:', response.credentialId);

3. Request Credentials (Gated Content)

const response = await learnCard.askCredentialSearch({
  query: [{
    type: 'QueryByTitle',
    credentialQuery: {
      reason: 'Verify your certification',
      title: 'JavaScript Expert'
    }
  }],
  challenge: `${Date.now()}-${Math.random()}`,
  domain: window.location.hostname
});

if (response.verifiablePresentation) {
  // User shared credentials - unlock content
  unlockPremiumFeatures();
}

4. Launch AI Tutor

await learnCard.launchFeature(
  '/ai/topics?shortCircuitStep=newTopic',
  'Explain how verifiable credentials work'
);

Technical Details

Package Structure

packages/learn-card-partner-connect-sdk/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ index.ts       # Main SDK class and factory
โ”‚   โ””โ”€โ”€ types.ts       # TypeScript type definitions
โ”œโ”€โ”€ dist/              # Build output (gitignored)
โ”‚   โ”œโ”€โ”€ partner-connect.js       # CommonJS
โ”‚   โ”œโ”€โ”€ partner-connect.esm.js   # ES Module
โ”‚   โ””โ”€โ”€ index.d.ts               # Type definitions
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ project.json       # Nx configuration
โ”œโ”€โ”€ rollup.config.js
โ”œโ”€โ”€ tsconfig.json
โ””โ”€โ”€ README.md         # Full documentation

Build System

  • Bundler: Rollup
  • Output: CJS + ESM + TypeScript declarations
  • Target: ES2019
  • Source maps: Yes
  • Size: ~8KB (minified)

Nx Integration

{
  "name": "partner-connect-sdk",
  "targets": {
    "build": "nx:run-script",
    "dev": "nx:run-script",
    "typecheck": "nx:run-script"
  }
}

Testing

The SDK has been validated with:

  • โœ… TypeScript compilation
  • โœ… Build output verification
  • โœ… Example app integration
  • โœ… All 7 postMessage actions working

Migration Guide

For existing partner apps using manual postMessage:

Step 1: Install SDK

{
  "dependencies": {
    "@learncard/partner-connect": "workspace:*"
  }
}

Step 2: Replace Manual Setup

Remove:

  • pendingRequests Map
  • sendPostMessage() helper
  • window.addEventListener('message', ...) listener
  • Request ID generation logic
  • Timeout management

Add:

import { createPartnerConnect } from '@learncard/partner-connect';

const learnCard = createPartnerConnect({
  hostOrigin: 'https://learncard.app'
});

Step 3: Update Method Calls

Replace all sendPostMessage(action, payload) calls with corresponding SDK methods:

// Before
await sendPostMessage('REQUEST_IDENTITY');
await sendPostMessage('SEND_CREDENTIAL', { credential });

// After
await learnCard.requestIdentity();
await learnCard.sendCredential(credential);

Browser Support

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+

Requires postMessage API and Promise support.

Breaking Changes

None - this is a new package with no existing consumers.

Documentation

  • ๐Ÿ“– SDK README: packages/learn-card-partner-connect-sdk/README.md
  • ๐Ÿ“– Migration Guide: examples/app-store-apps/1-basic-launchpad-app/SDK-MIGRATION.md
  • ๐Ÿ“– Example App README: examples/app-store-apps/1-basic-launchpad-app/README.md

Future Work

Potential enhancements for future PRs:

  • Unit tests with Jest/Vitest
  • Integration tests with Playwright
  • Retry logic for failed requests
  • Request batching/queuing
  • Event emitter for real-time updates
  • React hooks wrapper (useLearnCard)
  • Vue composable wrapper
  • Storybook stories

Checklist

  • [x] SDK implementation with all 7 methods
  • [x] Full TypeScript support
  • [x] Comprehensive documentation
  • [x] Example app demonstrating all features
  • [x] Environment variable support
  • [x] Error handling with proper error codes
  • [x] Origin validation
  • [x] Request timeout handling
  • [x] Build configuration (Rollup + Nx)
  • [x] Migration guide

Related Issues

Addresses the need for a standardized, type-safe way for partner applications to communicate with the LearnCard host application via postMessage.

Demo

See the example app at examples/app-store-apps/1-basic-launchpad-app/ for a complete working demonstration of all SDK capabilities.


Questions? Feel free to review the comprehensive documentation in the SDK's README or ask in the PR comments!

โœจ PR Description

Purpose: Add partner connectio

Custard7 avatar Oct 30 '25 21:10 Custard7

๐Ÿฆ‹ Changeset detected

Latest commit: 6381ac74f8d5b52524b93ecb66941e40c58d2ab6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@learncard/app-store-demo-basic-launchpad Patch
@learncard/app-store-demo-lore-card Patch
@learncard/partner-connect Patch
@learncard/app-store-demo-mozilla-social-badges Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

changeset-bot[bot] avatar Oct 30 '25 21:10 changeset-bot[bot]

Deploy Preview for learncarddocs canceled.

Name Link
Latest commit 6381ac74f8d5b52524b93ecb66941e40c58d2ab6
Latest deploy log https://app.netlify.com/projects/learncarddocs/deploys/69179a91c544ee00088c7a76

netlify[bot] avatar Oct 30 '25 21:10 netlify[bot]

Deploy Preview for lc-partner-connect-example-1 canceled.

Name Link
Latest commit 49f80250374df74cc4225379d4a4312007dd2cbd
Latest deploy log https://app.netlify.com/projects/lc-partner-connect-example-1/deploys/6904eb46e62eba0008d01413

netlify[bot] avatar Oct 31 '25 16:10 netlify[bot]

Deploy Preview for lore-card failed. Why did it fail? โ†’

Name Link
Latest commit ef395141e2d486569b23afb86bfa09dab366b5dc
Latest deploy log https://app.netlify.com/projects/lore-card/deploys/690525d6193d920008533770

netlify[bot] avatar Oct 31 '25 21:10 netlify[bot]

๐Ÿฅท Code experts: TaylorBeeston

Custard7 has most ๐Ÿ‘ฉโ€๐Ÿ’ป activity in the files. TaylorBeeston, Custard7 have most ๐Ÿง  knowledge in the files.

See details

pnpm-lock.yaml

Activity based on git-commit:

Custard7
NOV
OCT 152 additions & 19 deletions
SEP 837 additions & 295 deletions
AUG 43 additions & 0 deletions
JUL 315 additions & 59 deletions
JUN 68 additions & 190 deletions

Knowledge based on git-blame: TaylorBeeston: 72% Custard7: 23%

pnpm-workspace.yaml

Activity based on git-commit:

Custard7
NOV
OCT
SEP
AUG
JUL
JUN

Knowledge based on git-blame: TaylorBeeston: 100%

โœจ Comment /gs review for LinearB AI review. Learn how to automate it here.

gitstream-cm[bot] avatar Nov 06 '25 21:11 gitstream-cm[bot]