frontier-os-app-builder 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/agents/fos-executor.md +22 -65
  4. package/agents/fos-plan-checker.md +13 -12
  5. package/agents/fos-planner.md +20 -67
  6. package/agents/fos-researcher.md +14 -10
  7. package/agents/fos-verifier.md +11 -5
  8. package/bin/fos-tools.cjs +48 -11
  9. package/bin/install.js +8 -5
  10. package/commands/fos/add-feature.md +1 -2
  11. package/commands/fos/discuss.md +0 -1
  12. package/commands/fos/new-app.md +1 -3
  13. package/commands/fos/new-milestone.md +1 -1
  14. package/commands/fos/plan.md +0 -2
  15. package/package.json +7 -1
  16. package/references/app-patterns.md +46 -28
  17. package/references/deployment.md +40 -74
  18. package/references/module-index.md +32 -0
  19. package/references/sdk/chain.md +92 -0
  20. package/references/sdk/communities.md +159 -0
  21. package/references/sdk/events.md +212 -0
  22. package/references/sdk/init.md +126 -0
  23. package/references/sdk/navigation.md +49 -0
  24. package/references/sdk/offices.md +76 -0
  25. package/references/sdk/partnerships.md +111 -0
  26. package/references/sdk/storage.md +44 -0
  27. package/references/sdk/thirdparty.md +240 -0
  28. package/references/sdk/token-amount.md +99 -0
  29. package/references/sdk/types.md +27 -0
  30. package/references/sdk/ui-utils.md +39 -0
  31. package/references/sdk/user.md +208 -0
  32. package/references/sdk/wallet.md +334 -0
  33. package/references/verification-rules.md +18 -18
  34. package/templates/app/frontier-services.tsx +75 -18
  35. package/templates/app/layout.tsx +19 -9
  36. package/templates/app/package.json +2 -1
  37. package/templates/app/public/favicon.svg +3 -0
  38. package/templates/app/sdk-context.tsx +7 -9
  39. package/templates/app/sdk-services.tsx +92 -117
  40. package/templates/app/vercel.json +8 -47
  41. package/templates/state/plan.md +32 -14
  42. package/templates/state/roadmap.md +2 -2
  43. package/templates/state/summary.md +26 -29
  44. package/workflows/add-feature.md +6 -1
  45. package/workflows/discuss.md +9 -3
  46. package/workflows/execute-plan.md +3 -3
  47. package/workflows/execute.md +17 -6
  48. package/workflows/new-app.md +54 -18
  49. package/workflows/new-milestone.md +9 -2
  50. package/workflows/plan.md +14 -5
  51. package/workflows/ship.md +26 -10
  52. package/workflows/status.md +0 -1
  53. package/references/module-inference.md +0 -348
  54. package/references/sdk-surface.md +0 -1600
  55. package/templates/app/main-simple-standalone.tsx +0 -19
  56. package/templates/app/main-simple.tsx +0 -19
  57. package/templates/state/manifest.json +0 -12
package/bin/fos-tools.cjs CHANGED
@@ -10,7 +10,7 @@ const { execSync } = require('child_process');
10
10
  // Usage: node fos-tools.cjs <command> [args] [--raw] [--pick <field>]
11
11
  // ─────────────────────────────────────────────
12
12
 
13
- const VERSION = '1.0.0';
13
+ const VERSION = '1.2.0';
14
14
 
15
15
  // ── Helpers ──────────────────────────────────
16
16
 
@@ -270,8 +270,8 @@ const MODULE_KEYWORDS = {
270
270
  'withdraw', 'off-ramp', 'bank', 'fiat', 'subscription', 'billing', 'price',
271
271
  'cost', 'fee', 'tip', 'donate', 'donation'],
272
272
  getter: 'sdk.getWallet()',
273
- commonMethods: ['getBalance', 'getBalanceFormatted', 'transferFrontierDollar', 'transferOverallFrontierDollar'],
274
- permissions: ['wallet:getBalance', 'wallet:getBalanceFormatted', 'wallet:getAddress',
273
+ commonMethods: ['getBalance', 'transferFrontierDollar', 'transferOverallFrontierDollar'],
274
+ permissions: ['wallet:getBalance', 'wallet:getAddress',
275
275
  'wallet:transferFrontierDollar', 'wallet:transferOverallFrontierDollar']
276
276
  },
277
277
  User: {
@@ -287,9 +287,11 @@ const MODULE_KEYWORDS = {
287
287
  'reserve', 'reservation', 'space', 'venue', 'location', 'conference',
288
288
  'meeting', 'coworking'],
289
289
  getter: 'sdk.getEvents()',
290
- commonMethods: ['listEvents', 'createEvent', 'listLocations', 'createRoomBooking'],
290
+ commonMethods: ['listEvents', 'createEvent', 'listLocations', 'createRoomBooking',
291
+ 'getCryptoDepositPreflight', 'placeCryptoDeposit'],
291
292
  permissions: ['events:listEvents', 'events:createEvent', 'events:listLocations',
292
- 'events:listRoomBookings', 'events:createRoomBooking']
293
+ 'events:listRoomBookings', 'events:createRoomBooking',
294
+ 'events:getCryptoDepositPreflight', 'events:placeCryptoDeposit']
293
295
  },
294
296
  Communities: {
295
297
  keywords: ['community', 'group', 'team', 'club', 'internship', 'intern', 'cohort',
@@ -418,9 +420,12 @@ function cmdValidateStructure(cwd, flags) {
418
420
  // Determine verification tier from manifest sdkPhase
419
421
  const manifest = loadManifest(cwd);
420
422
  const sdkPhase = manifest && manifest.sdkPhase != null ? manifest.sdkPhase : null;
421
- const currentPhase = flags.phase ? parseInt(flags.phase, 10) : null;
423
+ const currentPhase = flags.phase != null && Number.isInteger(+flags.phase) ? +flags.phase : null;
424
+ // Fallback to STATE.md phase when --phase not passed, so the Tier-2 gate works even if a caller forgets the flag
425
+ const fallbackPhase = flags.phase == null ? (loadState(cwd)?.frontmatter?.phase ?? null) : null;
426
+ const effectivePhase = currentPhase ?? fallbackPhase;
422
427
  // Tier 2 only when sdkPhase is set AND current phase matches sdkPhase
423
- const isTier2 = sdkPhase != null && currentPhase != null && currentPhase === sdkPhase;
428
+ const isTier2 = sdkPhase != null && effectivePhase != null && effectivePhase === sdkPhase;
424
429
  // Backward compat: if no sdkPhase in manifest, run all checks (legacy SDK-first apps)
425
430
  const isLegacy = sdkPhase == null;
426
431
 
@@ -489,6 +494,9 @@ function cmdValidateStructure(cwd, flags) {
489
494
  if (!layout.includes('SdkProvider')) {
490
495
  issues.push('Layout.tsx missing SdkProvider wrapping');
491
496
  }
497
+ if (!layout.includes('FrontierServicesProvider')) {
498
+ issues.push('Layout.tsx missing FrontierServicesProvider bridge (useServices() will crash at runtime)');
499
+ }
492
500
  }
493
501
  }
494
502
 
@@ -540,8 +548,11 @@ function cmdValidatePermissions(cwd, flags) {
540
548
 
541
549
  // Determine tier
542
550
  const sdkPhase = manifest.sdkPhase != null ? manifest.sdkPhase : null;
543
- const currentPhase = flags.phase ? parseInt(flags.phase, 10) : null;
544
- const isTier2 = sdkPhase != null && currentPhase != null && currentPhase === sdkPhase;
551
+ const currentPhase = flags.phase != null && Number.isInteger(+flags.phase) ? +flags.phase : null;
552
+ // Fallback to STATE.md phase when --phase not passed, so the Tier-2 gate works even if a caller forgets the flag
553
+ const fallbackPhase = flags.phase == null ? (loadState(cwd)?.frontmatter?.phase ?? null) : null;
554
+ const effectivePhase = currentPhase ?? fallbackPhase;
555
+ const isTier2 = sdkPhase != null && effectivePhase != null && effectivePhase === sdkPhase;
545
556
  const isLegacy = sdkPhase == null;
546
557
 
547
558
  // Find all SDK method calls in source (both patterns)
@@ -588,7 +599,7 @@ function cmdValidatePermissions(cwd, flags) {
588
599
  for (const method of usedMethods) {
589
600
  // Try to find matching permission
590
601
  for (const [pattern, perm] of Object.entries(methodToPermission)) {
591
- if (method.includes(pattern.split('.')[0]) && !declaredPerms.has(perm)) {
602
+ if (method === pattern && !declaredPerms.has(perm)) {
592
603
  missingPerms.push({ method, permission: perm });
593
604
  }
594
605
  }
@@ -768,13 +779,14 @@ function cmdCommit(message, files, flags) {
768
779
 
769
780
  function main() {
770
781
  const args = process.argv.slice(2);
771
- const flags = { raw: false, pick: null };
782
+ const flags = { raw: false, pick: null, phase: null };
772
783
 
773
784
  // Extract flags
774
785
  const cleanArgs = [];
775
786
  for (let i = 0; i < args.length; i++) {
776
787
  if (args[i] === '--raw') { flags.raw = true; }
777
788
  else if (args[i] === '--pick' && args[i + 1]) { flags.pick = args[++i]; }
789
+ else if (args[i] === '--phase' && args[i + 1]) { flags.phase = args[++i]; }
778
790
  else { cleanArgs.push(args[i]); }
779
791
  }
780
792
 
@@ -853,6 +865,31 @@ Commands:
853
865
  break;
854
866
  }
855
867
 
868
+ case 'sdk-ref': {
869
+ const modulesIdx = rest.indexOf('--modules');
870
+ if (modulesIdx < 0 || !rest[modulesIdx + 1]) {
871
+ error('sdk-ref requires --modules <Module1,Module2,...>');
872
+ }
873
+ const modules = rest[modulesIdx + 1].split(',').map(m => m.trim().toLowerCase());
874
+ const fosHome = process.env.FOS_HOME || path.join(require('os').homedir(), '.claude', 'frontier-os-app-builder');
875
+ const sdkDir = path.join(fosHome, 'references', 'sdk');
876
+ const always = ['init', 'types'];
877
+ const allFiles = [...always, ...modules];
878
+ const parts = [];
879
+ for (const f of allFiles) {
880
+ const fp = path.join(sdkDir, `${f}.md`);
881
+ if (!fs.existsSync(fp)) {
882
+ error(`SDK reference file not found: ${fp}`);
883
+ }
884
+ parts.push(fs.readFileSync(fp, 'utf-8'));
885
+ }
886
+ const content = parts.join('\n\n---\n\n');
887
+ const tmpPath = path.join(require('os').tmpdir(), `fos-sdk-ref-${Date.now()}.md`);
888
+ fs.writeFileSync(tmpPath, content);
889
+ console.log(`@file:${tmpPath}`);
890
+ break;
891
+ }
892
+
856
893
  default:
857
894
  error(`Unknown command: ${command}. Run 'node fos-tools.cjs help' for usage.`);
858
895
  }
package/bin/install.js CHANGED
@@ -10,7 +10,7 @@ const path = require('path');
10
10
  // and templates into ~/.claude/
11
11
  // ─────────────────────────────────────────────
12
12
 
13
- const VERSION = '1.0.0';
13
+ const VERSION = '1.2.0';
14
14
  const PRODUCT = 'frontier-os-app-builder';
15
15
 
16
16
  // Source: this repo
@@ -104,15 +104,16 @@ function install() {
104
104
  log('Installing agents...');
105
105
  const agentSrc = path.join(SRC_ROOT, 'agents');
106
106
  const agentDest = path.join(CLAUDE_HOME, 'agents');
107
+ let agentCount = 0;
107
108
  if (fs.existsSync(agentSrc)) {
108
109
  for (const file of fs.readdirSync(agentSrc)) {
109
110
  if (file.startsWith('fos-') && file.endsWith('.md')) {
110
111
  const dest = copyFile(path.join(agentSrc, file), path.join(agentDest, file));
111
112
  installedFiles.push(dest);
113
+ agentCount++;
112
114
  }
113
115
  }
114
116
  }
115
- const agentCount = installedFiles.filter(f => f.includes('/agents/fos-')).length;
116
117
  success(`${agentCount} agents → ~/.claude/agents/`);
117
118
 
118
119
  // 3. Workflows → ~/.claude/frontier-os-app-builder/workflows/
@@ -149,7 +150,8 @@ function install() {
149
150
  success(`CLI tool → ~/.claude/${PRODUCT}/bin/fos-tools.cjs`);
150
151
  }
151
152
 
152
- // 7. Write manifest
153
+ // 7. Write manifest (record its own path first so uninstall removes it)
154
+ installedFiles.push(MANIFEST_PATH);
153
155
  const manifest = {
154
156
  version: VERSION,
155
157
  installed_at: new Date().toISOString(),
@@ -157,7 +159,6 @@ function install() {
157
159
  };
158
160
  ensureDir(path.dirname(MANIFEST_PATH));
159
161
  fs.writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
160
- installedFiles.push(MANIFEST_PATH);
161
162
 
162
163
  // Summary
163
164
  console.log(`\n\x1b[32m ✓ Installed ${installedFiles.length} files\x1b[0m\n`);
@@ -185,11 +186,13 @@ function uninstall() {
185
186
  }
186
187
  }
187
188
 
188
- // Clean up directories
189
+ // Clean up directories (deepest-first: prune child dirs before their parents)
189
190
  const dirsToClean = [
190
191
  path.join(CLAUDE_HOME, 'commands', 'fos'),
191
192
  path.join(FOS_HOME, 'workflows'),
193
+ path.join(FOS_HOME, 'references', 'sdk'),
192
194
  path.join(FOS_HOME, 'references'),
195
+ path.join(FOS_HOME, 'templates', 'app', 'public'),
193
196
  path.join(FOS_HOME, 'templates', 'app'),
194
197
  path.join(FOS_HOME, 'templates', 'state'),
195
198
  path.join(FOS_HOME, 'templates'),
@@ -16,8 +16,7 @@ Add a new feature to the current milestone. Infers required SDK modules, creates
16
16
 
17
17
  <execution_context>
18
18
  @$HOME/.claude/frontier-os-app-builder/workflows/add-feature.md
19
- @$HOME/.claude/frontier-os-app-builder/references/module-inference.md
20
- @$HOME/.claude/frontier-os-app-builder/references/sdk-surface.md
19
+ @$HOME/.claude/frontier-os-app-builder/references/module-index.md
21
20
  </execution_context>
22
21
 
23
22
  <context>
@@ -19,7 +19,6 @@ Gather implementation decisions for a phase by identifying gray areas and discus
19
19
 
20
20
  <execution_context>
21
21
  @$HOME/.claude/frontier-os-app-builder/workflows/discuss.md
22
- @$HOME/.claude/frontier-os-app-builder/references/sdk-surface.md
23
22
  </execution_context>
24
23
 
25
24
  <context>
@@ -24,9 +24,7 @@ Initialize a new Frontier OS app through guided flow: gather requirements, infer
24
24
 
25
25
  <execution_context>
26
26
  @$HOME/.claude/frontier-os-app-builder/workflows/new-app.md
27
- @$HOME/.claude/frontier-os-app-builder/references/sdk-surface.md
28
- @$HOME/.claude/frontier-os-app-builder/references/module-inference.md
29
- @$HOME/.claude/frontier-os-app-builder/references/app-patterns.md
27
+ @$HOME/.claude/frontier-os-app-builder/references/module-index.md
30
28
  </execution_context>
31
29
 
32
30
  <context>
@@ -16,7 +16,7 @@ Archive the current milestone and start a new one. Gathers new feature descripti
16
16
 
17
17
  <execution_context>
18
18
  @$HOME/.claude/frontier-os-app-builder/workflows/new-milestone.md
19
- @$HOME/.claude/frontier-os-app-builder/references/module-inference.md
19
+ @$HOME/.claude/frontier-os-app-builder/references/module-index.md
20
20
  </execution_context>
21
21
 
22
22
  <context>
@@ -23,8 +23,6 @@ Create execution plans for a phase by researching existing Frontier OS apps, the
23
23
 
24
24
  <execution_context>
25
25
  @$HOME/.claude/frontier-os-app-builder/workflows/plan.md
26
- @$HOME/.claude/frontier-os-app-builder/references/sdk-surface.md
27
- @$HOME/.claude/frontier-os-app-builder/references/app-patterns.md
28
26
  </execution_context>
29
27
 
30
28
  <context>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontier-os-app-builder",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Meta-prompting framework for building Frontier OS apps with Claude Code",
5
5
  "author": "BerlinhouseLabs",
6
6
  "license": "MIT",
@@ -16,6 +16,12 @@
16
16
  "templates/",
17
17
  "README.md"
18
18
  ],
19
+ "scripts": {
20
+ "prepublishOnly": "node --check bin/install.js && node bin/fos-tools.cjs version"
21
+ },
22
+ "engines": {
23
+ "node": ">=18"
24
+ },
19
25
  "keywords": [
20
26
  "frontier-os",
21
27
  "claude-code",
@@ -13,7 +13,7 @@ Reference for the standard structure, conventions, and tech stack used by all Fr
13
13
  | Language | TypeScript | 5.9 |
14
14
  | CSS | Tailwind CSS (via PostCSS) | 4 |
15
15
  | Testing | Vitest + jsdom + Testing Library| 4 / 27 |
16
- | SDK | @frontiertower/frontier-sdk | 0.21.0 |
16
+ | SDK | @frontiertower/frontier-sdk | 0.24.0 |
17
17
  | Routing | react-router-dom | 7 |
18
18
 
19
19
  ---
@@ -69,12 +69,12 @@ These files are copied verbatim. They must not be modified per app.
69
69
 
70
70
  ### `src/lib/frontier-services.tsx`
71
71
 
72
- This file provides the `useServices()` hook and `FrontierServicesProvider`. It is identical across all apps. During standalone development it returns mock services; after SDK Integration it detects the environment and returns either mocks or real SDK-backed services.
72
+ This file provides the `useServices()` hook and `FrontierServicesProvider`. It is identical across all apps and stays **SDK-free** (imports only React) — it is the mock seam. During standalone development `FrontierServicesProvider` returns mock services; after SDK Integration the **Layout** passes it real SDK-backed services in-frame (mocks standalone), so feature code never changes.
73
73
 
74
74
  ### `src/lib/sdk-context.tsx` — identical across all apps, created during SDK Integration phase (not at scaffold time)
75
75
 
76
76
  ```tsx
77
- import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from 'react';
77
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
78
78
  import { FrontierSDK } from '@frontiertower/frontier-sdk';
79
79
 
80
80
  const SdkContext = createContext<FrontierSDK | null>(null);
@@ -86,29 +86,29 @@ export const useSdk = (): FrontierSDK => {
86
86
  };
87
87
 
88
88
  export const SdkProvider = ({ children }: { children: ReactNode }) => {
89
- const sdkRef = useRef<FrontierSDK | null>(null);
90
- const [ready, setReady] = useState(false);
89
+ const [sdk, setSdk] = useState<FrontierSDK | null>(null);
91
90
 
92
91
  useEffect(() => {
93
- const sdk = new FrontierSDK();
94
- sdkRef.current = sdk;
95
- setReady(true);
92
+ const instance = new FrontierSDK();
93
+ setSdk(instance);
96
94
 
97
95
  return () => {
98
- sdk.destroy();
96
+ instance.destroy();
99
97
  };
100
98
  }, []);
101
99
 
102
- if (!ready) return null;
100
+ if (!sdk) return null;
103
101
 
104
102
  return (
105
- <SdkContext.Provider value={sdkRef.current}>
103
+ <SdkContext.Provider value={sdk}>
106
104
  {children}
107
105
  </SdkContext.Provider>
108
106
  );
109
107
  };
110
108
  ```
111
109
 
110
+ > The SDK is held in `useState` (not a `useRef`): under React StrictMode the effect runs mount → cleanup → mount, so the first instance is created then destroyed. Storing it in state makes the second `setSdk(...)` re-render the Provider with the live instance, instead of leaving consumers pinned to the destroyed one. Do not "simplify" this back to a ref.
111
+
112
112
  ### `src/lib/sdk-services.tsx` — identical across all apps, created during SDK Integration phase (not at scaffold time)
113
113
 
114
114
  This file provides the adapter that maps the `FrontierServices` interface to real SDK calls. It is created during the SDK Integration phase.
@@ -152,7 +152,7 @@ export default {
152
152
 
153
153
  ### `vercel.json`
154
154
 
155
- See [deployment.md](deployment.md) for the full file. All apps share the same CORS configuration with 3 origin blocks.
155
+ See [deployment.md](deployment.md) for the full file. All apps share the same `vercel.json`: CORS for the production origin plus a `Content-Security-Policy: frame-ancestors` listing the 3 live Frontier OS origins (production `os.frontiertower.io`, sandbox `sandbox.os.frontiertower.io`, and `localhost:5173`) and the standard security headers.
156
156
 
157
157
  ---
158
158
 
@@ -175,7 +175,7 @@ Fixed fields (do not change):
175
175
  - `"private": true`
176
176
  - `"type": "module"`
177
177
  - `scripts` block (see Package Scripts below)
178
- - Core dependencies: `@frontiertower/frontier-sdk`, `react`, `react-dom`, `react-router-dom`
178
+ - Core dependencies: `@frontiertower/frontier-sdk`, `react`, `react-dom`, `react-router-dom`, `viem` (`^2.44.0` — for on-chain apps that build calldata for `executeCall`/`executeBatchCall`; safe to drop for pure-UI apps)
179
179
  - Core devDependencies: `@tailwindcss/postcss`, `@types/react`, `@types/react-dom`, `@vitejs/plugin-react`, `postcss`, `tailwindcss`, `typescript`, `vite`
180
180
  - Test devDependencies (when tests exist): `@testing-library/jest-dom`, `@testing-library/react`, `@testing-library/user-event`, `@vitest/coverage-v8`, `jsdom`, `vitest`
181
181
 
@@ -318,9 +318,14 @@ export const Layout = () => {
318
318
  );
319
319
  }
320
320
 
321
+ // In-frame: SdkProvider provides the SDK; SdkServicesBridge wires it into
322
+ // FrontierServicesProvider so feature code's useServices() works unchanged.
323
+ // (SdkServicesBridge helper — see templates/app/layout.tsx.)
321
324
  return (
322
325
  <SdkProvider>
323
- <Outlet />
326
+ <SdkServicesBridge>
327
+ <Outlet />
328
+ </SdkServicesBridge>
324
329
  </SdkProvider>
325
330
  );
326
331
  };
@@ -347,7 +352,7 @@ No iframe detection, no loading state, no standalone fallback. The app just rend
347
352
 
348
353
  #### SDK-Aware Layout (after SDK Integration phase)
349
354
 
350
- The existing Layout pattern with `isInFrontierApp()`, `createStandaloneHTML()`, and `SdkProvider` wrapping is applied during the SDK Integration phase. See the current Layout Pattern above.
355
+ The Layout pattern with `isInFrontierApp()`, `createStandaloneHTML()`, `SdkProvider`, AND the `FrontierServicesProvider` bridge (so `useServices()` works against the real SDK) is applied during the SDK Integration phase. See the SDK-Aware Layout Pattern above and `templates/app/layout.tsx`.
351
356
 
352
357
  ---
353
358
 
@@ -359,20 +364,21 @@ New apps use the `useServices()` abstraction instead of `useSdk()` directly. Thi
359
364
 
360
365
  ```typescript
361
366
  import { useState, useEffect } from 'react';
367
+ import { formatAmount } from '@frontiertower/frontier-sdk';
362
368
  import { useServices } from '../lib/frontier-services';
363
- import type { WalletBalanceFormatted } from '../lib/frontier-services';
364
369
 
365
370
  export function useBalance() {
366
371
  const services = useServices();
367
- const [balance, setBalance] = useState<WalletBalanceFormatted | null>(null);
372
+ // Balance fields are bigint base units; format them for display with formatAmount().
373
+ const [balance, setBalance] = useState<string | null>(null);
368
374
  const [loading, setLoading] = useState(true);
369
375
  const [error, setError] = useState<string | null>(null);
370
376
 
371
377
  useEffect(() => {
372
378
  const fetch = async () => {
373
379
  try {
374
- const result = await services.wallet.getBalanceFormatted();
375
- setBalance(result);
380
+ const result = await services.wallet.getBalance();
381
+ setBalance(formatAmount(result.total));
376
382
  } catch (err) {
377
383
  setError(err instanceof Error ? err.message : 'Failed to load balance');
378
384
  } finally {
@@ -417,13 +423,25 @@ export const router = createBrowserRouter([
417
423
  ]);
418
424
  ```
419
425
 
420
- ### Single-Component Variant
426
+ ### Simple Apps (Single View)
427
+
428
+ Even an app with only one view uses the router — there is no "render the Layout directly from `main.tsx`" path. Define a single index route so `Layout` stays in the render tree:
429
+
430
+ ```tsx
431
+ import { createBrowserRouter } from 'react-router-dom';
432
+ import { Layout } from './views/Layout';
433
+ import { Home } from './views/Home';
434
+
435
+ export const router = createBrowserRouter([
436
+ {
437
+ path: '/',
438
+ element: <Layout />,
439
+ children: [{ index: true, element: <Home /> }],
440
+ },
441
+ ]);
442
+ ```
421
443
 
422
- For very simple apps that need only one view, the router can be omitted. In this case:
423
- - `main.tsx` renders the Layout directly instead of `<RouterProvider>`
424
- - `Layout.tsx` renders the single view component instead of `<Outlet />`
425
- - No `router.tsx` file needed
426
- - `react-router-dom` can be removed from dependencies
444
+ Keeping `Layout` mounted preserves the services seam: standalone it provides `FrontierServicesProvider`, and after SDK Integration it wraps the app in `SdkProvider` + `SdkServicesBridge` so `useServices()` resolves against the real SDK. `react-router-dom` stays a dependency. Do not bypass the router by rendering a view straight from `main.tsx` — that skips the Layout bridge and ships mock services in-frame.
427
445
 
428
446
  ### `main.tsx` (Identical Pattern)
429
447
 
@@ -581,9 +599,9 @@ The final phase of every app wires the real Frontier SDK in. This is a mechanica
581
599
  1. **Add SDK dependency**: `npm install @frontiertower/frontier-sdk`
582
600
  2. **Create `src/lib/sdk-context.tsx`**: Standard SdkProvider + useSdk hook (from template)
583
601
  3. **Create `src/lib/sdk-services.tsx`**: Adapter mapping FrontierServices interface to real SDK calls
584
- 4. **Upgrade `src/lib/frontier-services.tsx`**: Add environment detection iframe uses SDK adapter, standalone uses mocks
585
- 5. **Upgrade `src/views/Layout.tsx`**: Add `isInFrontierApp()` detection, standalone fallback, `SdkProvider` wrapping
586
- 6. **Add CORS origins to `vercel.json`**: All 3 Frontier OS origins
602
+ 4. **Leave `src/lib/frontier-services.tsx` unchanged**: it stays the SDK-free mock seam. The iframe/standalone switch happens in Layout (step 5) — do NOT add SDK imports or detection here.
603
+ 5. **Swap in `src/views/Layout.tsx`** (from `templates/app/layout.tsx`): `isInFrontierApp()` detection + standalone fallback; in-frame it wraps the app in `SdkProvider` AND bridges the SDK into `FrontierServicesProvider` (via `createSdkServices(sdk)`) so `useServices()` resolves against the real SDK
604
+ 6. **Swap in the full `vercel.json`**: CORS for the production origin + `Content-Security-Policy: frame-ancestors` listing the 3 live origins (`os.frontiertower.io`, `sandbox.os.frontiertower.io`, `localhost:5173`) + security headers
587
605
 
588
606
  After SDK Integration, the app works in both modes:
589
607
  - **Standalone** (browser): Uses mock services, shows development data