Skip to content
Open
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
24 changes: 24 additions & 0 deletions docs/demo-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Full Demo Mode

Bridgelet provides a comprehensive "Demo Mode" built entirely on top of MSW (Mock Service Worker). This mode intercepts all backend SDK API calls (`/send`, `/claim/:token`, `/claim/:token/redeem`) directly in the browser and returns mocked responses.

## Why use Demo Mode?
- **Conferences & Demos:** Confidently demo the entire send and claim flows without needing a live backend, database, or risking network timeouts.
- **Onboarding:** Allows new front-end contributors to run and test the UI immediately after cloning the repository without setting up the entire backend stack.
- **UI/UX Testing:** Instantly generate mock claims to test different edge cases in the UI.

## How to use it
To trigger demo mode, simply append the `?demo=true` query parameter to the URL in your browser:

```
http://localhost:3000/send?demo=true
```

1. Navigate to the Send page with the flag.
2. Fill out the mock payment details and confirm.
3. The mock API will intercept the `createPaymentIntent` request and return a mock `claimUrl` that persists the `?demo=true` flag.
4. Clicking or navigating to the mock claim URL will intercept the `getClaimDetails` and `redeemClaim` requests, simulating a successful claim flow!

## Limitations
- **Ephemeral State:** Because the "database" is just the MSW worker running in memory, refreshing the page after creating a claim will lose that claim's state.
- **No Real Transactions:** The mock redeemer returns a hardcoded fake transaction hash (`mock-tx-hash-0987654321`) which will 404 if you attempt to view it on the Stellar Explorer.
7 changes: 4 additions & 3 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en">
<body>
{children}
{isDev && <DevToolbar />}
{isDev && <MockProvider />}
<MockProvider>
{children}
{isDev && <DevToolbar />}
</MockProvider>
</body>
</html>
);
Expand Down
20 changes: 15 additions & 5 deletions frontend/components/mock-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
'use client';

import { useEffect } from 'react';
import { useEffect, useState } from 'react';

export function MockProvider({ children }: { children: React.ReactNode }) {
const [ready, setReady] = useState(false);

export function MockProvider() {
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
import('@/mocks').then(({ initMocks }) => initMocks());
const isDev = process.env.NODE_ENV === 'development';
const isDemo = typeof window !== 'undefined' && window.location.search.includes('demo=true');

if (isDev || isDemo) {
import('@/mocks').then(({ initMocks }) => {
initMocks().then(() => setReady(true));
});
} else {
setReady(true);
}
}, []);

return null;
if (!ready) return null;
return <>{children}</>;
}
14 changes: 12 additions & 2 deletions frontend/components/send-form/steps/confirm-step.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useState } from 'react';
import type { SendFormState } from '../index';
import { createPaymentIntent } from '@/lib/bridgelet';

type ConfirmStepProps = {
state: SendFormState;
Expand All @@ -12,13 +13,19 @@ export function ConfirmStep({ state, onBack }: ConfirmStepProps) {
const [submitting, setSubmitting] = useState(false);
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
const [claimUrl, setClaimUrl] = useState<string | null>(null);

async function handleConfirm() {
setSubmitting(true);
setError(null);
try {
// Placeholder: wire up to POST /api/accounts + POST /send in a real impl.
await new Promise((res) => setTimeout(res, 800));
const response = await createPaymentIntent({
senderPublicKey: state.publicKey,
amountStroops: (parseFloat(state.amountXlm) * 10_000_000).toString(),
assetCode: state.assetCode,
memo: state.memo,
});
setClaimUrl(response.claimUrl);
setSubmitted(true);
} catch (err) {
setError(err instanceof Error ? err.message : 'Something went wrong.');
Expand All @@ -39,6 +46,9 @@ export function ConfirmStep({ state, onBack }: ConfirmStepProps) {
A claim link has been sent to <strong>{state.recipientEmail}</strong>. They have 24
hours to claim their funds.
</p>
<p className="mt-4 break-all text-sm font-mono text-slate-700 bg-slate-100 p-2 rounded border border-slate-200">
{claimUrl}
</p>
</div>
);
}
Expand Down
7 changes: 6 additions & 1 deletion frontend/lib/bridgelet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,16 @@ export interface ApiError {
statusCode: number;
}

export {
import {
BridgeletClient,
type BridgeletClientOptions,
} from '@/lib/create-bridgelet-client';

export {
BridgeletClient,
type BridgeletClientOptions,
};

let _defaultClient: BridgeletClient | null = null;

function defaultClient(): BridgeletClient {
Expand Down
6 changes: 3 additions & 3 deletions frontend/lib/wallet → frontend/lib/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ export async function connectFreighter(): Promise<ConnectedWallet> {
// Request access — this opens the Freighter popup
await freighter.requestAccess();

const { publicKey } = await freighter.getPublicKey();
if (!publicKey) {
const { address } = await freighter.getAddress();
if (!address) {
throw new Error("Freighter did not return a public key. Did you approve the request?");
}

return { publicKey, type: "freighter" };
return { publicKey: address, type: "freighter" };
}

// LOBSTR is mobile-only, so on desktop we deeplink and poll for a result
Expand Down
4 changes: 3 additions & 1 deletion frontend/mocks/browser.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { setupWorker } from 'msw/browser';
import { accountHandlers } from './handlers/accounts';
import { claimsHandlers } from './handlers/claims';
import { horizonHandlers } from './handlers/horizon';
import { apiHandlers } from './handlers/api';

/**
* MSW service worker for browser environments.
*
* Start this once in development to intercept fetch/XHR calls.
* The worker is only initialised when this module is imported.
*/
export const worker = setupWorker(...accountHandlers, ...horizonHandlers, ...claimsHandlers);
export const worker = setupWorker(...accountHandlers, ...horizonHandlers, ...claimsHandlers, ...apiHandlers);
32 changes: 32 additions & 0 deletions frontend/mocks/handlers/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { http, HttpResponse } from 'msw';

export const apiHandlers = [
// Mock POST /send (Create Payment Intent)
http.post('*/send', async () => {
return HttpResponse.json({
intentId: 'mock-intent-1234',
claimToken: 'demo-token-123',
claimUrl: 'http://localhost:3000/claim/demo-token-123?demo=true',
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
});
}),

// Mock GET /claim/:token (Get Claim Details)
http.get('*/claim/:token', ({ params }) => {
return HttpResponse.json({
valid: true,
amountStroops: '500000000', // 50 XLM
assetCode: 'XLM',
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
memo: 'Demo payment',
});
}),

// Mock POST /claim/:token/redeem (Redeem Claim)
http.post('*/claim/:token/redeem', async () => {
return HttpResponse.json({
txHash: 'mock-tx-hash-0987654321',
explorerUrl: 'https://stellar.expert/explorer/testnet/tx/mock-tx-hash-0987654321',
});
}),
];
2 changes: 1 addition & 1 deletion frontend/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import "./.next/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
Loading
Loading