LearnCard
LearnCard copied to clipboard
feat [LC-1451]: Add partner connect SDK and example app
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:
- SSO Authentication -
requestIdentity()for user sign-on - Credential Issuance -
sendCredential()for issuing VCs - Feature Launching -
launchFeature()to navigate host app - Credential Requests -
askCredentialSearch()for querying user credentials - Specific Credential Access -
askCredentialSpecific()by ID - Consent Management -
requestConsent()for permissions - 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 outLC_UNAUTHENTICATED- User not logged inUSER_REJECTED- User declined the requestCREDENTIAL_NOT_FOUND- Credential doesn't existUNAUTHORIZED- User lacks permissionTEMPLATE_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:
pendingRequestsMapsendPostMessage()helperwindow.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
๐ฆ 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
Deploy Preview for learncarddocs canceled.
| Name | Link |
|---|---|
| Latest commit | 6381ac74f8d5b52524b93ecb66941e40c58d2ab6 |
| Latest deploy log | https://app.netlify.com/projects/learncarddocs/deploys/69179a91c544ee00088c7a76 |
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 |
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 |
๐ฅท 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.