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.
- package/LICENSE +21 -0
- package/README.md +25 -0
- package/agents/fos-executor.md +22 -65
- package/agents/fos-plan-checker.md +13 -12
- package/agents/fos-planner.md +20 -67
- package/agents/fos-researcher.md +14 -10
- package/agents/fos-verifier.md +11 -5
- package/bin/fos-tools.cjs +48 -11
- package/bin/install.js +8 -5
- package/commands/fos/add-feature.md +1 -2
- package/commands/fos/discuss.md +0 -1
- package/commands/fos/new-app.md +1 -3
- package/commands/fos/new-milestone.md +1 -1
- package/commands/fos/plan.md +0 -2
- package/package.json +7 -1
- package/references/app-patterns.md +46 -28
- package/references/deployment.md +40 -74
- package/references/module-index.md +32 -0
- package/references/sdk/chain.md +92 -0
- package/references/sdk/communities.md +159 -0
- package/references/sdk/events.md +212 -0
- package/references/sdk/init.md +126 -0
- package/references/sdk/navigation.md +49 -0
- package/references/sdk/offices.md +76 -0
- package/references/sdk/partnerships.md +111 -0
- package/references/sdk/storage.md +44 -0
- package/references/sdk/thirdparty.md +240 -0
- package/references/sdk/token-amount.md +99 -0
- package/references/sdk/types.md +27 -0
- package/references/sdk/ui-utils.md +39 -0
- package/references/sdk/user.md +208 -0
- package/references/sdk/wallet.md +334 -0
- package/references/verification-rules.md +18 -18
- package/templates/app/frontier-services.tsx +75 -18
- package/templates/app/layout.tsx +19 -9
- package/templates/app/package.json +2 -1
- package/templates/app/public/favicon.svg +3 -0
- package/templates/app/sdk-context.tsx +7 -9
- package/templates/app/sdk-services.tsx +92 -117
- package/templates/app/vercel.json +8 -47
- package/templates/state/plan.md +32 -14
- package/templates/state/roadmap.md +2 -2
- package/templates/state/summary.md +26 -29
- package/workflows/add-feature.md +6 -1
- package/workflows/discuss.md +9 -3
- package/workflows/execute-plan.md +3 -3
- package/workflows/execute.md +17 -6
- package/workflows/new-app.md +54 -18
- package/workflows/new-milestone.md +9 -2
- package/workflows/plan.md +14 -5
- package/workflows/ship.md +26 -10
- package/workflows/status.md +0 -1
- package/references/module-inference.md +0 -348
- package/references/sdk-surface.md +0 -1600
- package/templates/app/main-simple-standalone.tsx +0 -19
- package/templates/app/main-simple.tsx +0 -19
- 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.
|
|
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', '
|
|
274
|
-
permissions: ['wallet:getBalance', 'wallet:
|
|
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
|
|
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 &&
|
|
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
|
|
544
|
-
|
|
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
|
|
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.
|
|
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-
|
|
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>
|
package/commands/fos/discuss.md
CHANGED
|
@@ -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>
|
package/commands/fos/new-app.md
CHANGED
|
@@ -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/
|
|
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-
|
|
19
|
+
@$HOME/.claude/frontier-os-app-builder/references/module-index.md
|
|
20
20
|
</execution_context>
|
|
21
21
|
|
|
22
22
|
<context>
|
package/commands/fos/plan.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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,
|
|
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
|
|
90
|
-
const [ready, setReady] = useState(false);
|
|
89
|
+
const [sdk, setSdk] = useState<FrontierSDK | null>(null);
|
|
91
90
|
|
|
92
91
|
useEffect(() => {
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
setReady(true);
|
|
92
|
+
const instance = new FrontierSDK();
|
|
93
|
+
setSdk(instance);
|
|
96
94
|
|
|
97
95
|
return () => {
|
|
98
|
-
|
|
96
|
+
instance.destroy();
|
|
99
97
|
};
|
|
100
98
|
}, []);
|
|
101
99
|
|
|
102
|
-
if (!
|
|
100
|
+
if (!sdk) return null;
|
|
103
101
|
|
|
104
102
|
return (
|
|
105
|
-
<SdkContext.Provider value={
|
|
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
|
|
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
|
-
<
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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. **
|
|
585
|
-
5. **
|
|
586
|
-
6. **
|
|
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
|