@africode/core 5.0.7 → 5.0.9

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 (64) hide show
  1. package/AGENTS.md +2 -0
  2. package/AGENT_INSTRUCTIONS.md +595 -595
  3. package/COMPONENT_SCHEMA.json +1800 -990
  4. package/README.md +2 -0
  5. package/bin/create-africode.js +87 -25
  6. package/components/auth-form.js +154 -0
  7. package/components/index.js +15 -0
  8. package/components/kyc-upload.js +173 -0
  9. package/components/nav-drawer.js +217 -0
  10. package/components/transaction-ledger.js +138 -0
  11. package/components/wallet-balance.js +114 -0
  12. package/core/a2ui-schema-manager.js +1 -1
  13. package/core/a2ui.js +178 -1
  14. package/core/bun-runtime.js +122 -7
  15. package/core/cli/commands/build.js +30 -5
  16. package/core/cli/ui.js +13 -3
  17. package/core/compliance.js +201 -0
  18. package/core/middleware.js +80 -17
  19. package/core/patterns.js +168 -0
  20. package/core/request-analytics.js +254 -0
  21. package/core/request-identity.js +79 -29
  22. package/core/sdk.js +4 -1
  23. package/core/validation.js +13 -0
  24. package/dist/africode.js +858 -457
  25. package/dist/africode.js.map +14 -9
  26. package/dist/build-info.json +3 -3
  27. package/dist/components.js +784 -383
  28. package/dist/components.js.map +11 -6
  29. package/package.json +1 -1
  30. package/templates/starter/package.json +5 -5
  31. package/templates/starter/src/index.js +18 -0
  32. package/templates/starter/src/pages/index.js +1 -1
  33. package/templates/starter-3d/package.json +5 -5
  34. package/templates/starter-3d/src/pages/index.js +1 -1
  35. package/templates/starter-dashboard/.env.example +21 -0
  36. package/templates/starter-dashboard/africode.config.js +20 -0
  37. package/templates/starter-dashboard/package.json +14 -0
  38. package/templates/starter-dashboard/src/index.js +17 -0
  39. package/templates/starter-dashboard/src/pages/api/analytics.js +24 -0
  40. package/templates/starter-dashboard/src/pages/index.html +118 -0
  41. package/templates/starter-dashboard/src/pages/index.js +110 -0
  42. package/templates/starter-dashboard/src/styles/main.css +172 -0
  43. package/templates/starter-fintech/.env.example +28 -0
  44. package/templates/starter-fintech/africode.config.js +20 -0
  45. package/templates/starter-fintech/package.json +15 -0
  46. package/templates/starter-fintech/src/index.js +17 -0
  47. package/templates/starter-fintech/src/pages/api/auth.js +45 -0
  48. package/templates/starter-fintech/src/pages/api/transfer.js +65 -0
  49. package/templates/starter-fintech/src/pages/api/wallet.js +39 -0
  50. package/templates/starter-fintech/src/pages/api/webhooks/payment.js +32 -0
  51. package/templates/starter-fintech/src/pages/index.html +169 -0
  52. package/templates/starter-fintech/src/pages/index.js +161 -0
  53. package/templates/starter-fintech/src/styles/main.css +246 -0
  54. package/templates/starter-react/package.json +5 -5
  55. package/templates/starter-react/src/pages/index.js +1 -1
  56. package/templates/starter-tailwind/package.json +5 -5
  57. package/templates/starter-tailwind/src/pages/index.js +1 -1
  58. package/templates/starter-website/.env.example +18 -0
  59. package/templates/starter-website/africode.config.js +20 -0
  60. package/templates/starter-website/package.json +14 -0
  61. package/templates/starter-website/src/index.js +17 -0
  62. package/templates/starter-website/src/pages/index.html +124 -0
  63. package/templates/starter-website/src/pages/index.js +116 -0
  64. package/templates/starter-website/src/styles/main.css +195 -0
package/README.md CHANGED
@@ -349,6 +349,8 @@ Then use them in your HTML:
349
349
 
350
350
  AfriCode now includes a native UI kit with shadcn-inspired Web Components for rapid interface design. The Tailwind starter demonstrates the new UI kit using components like `af-ui-button`, `af-ui-card`, `af-ui-input`, `af-ui-badge`, and `af-ui-switch` in a Shadow DOM-friendly workflow.
351
351
 
352
+ All `af-`, `af-ui-`, and `af-dashboard-` components are distributed together in the same `@africode/core` package, so they share theme, styles, registry, and versioning.
353
+
352
354
  The library is optimized for consistent spacing, elevation, and interactive states across theme-aware apps.
353
355
 
354
356
  Use the Tailwind starter template for a ready-made UI experience:
@@ -27,6 +27,42 @@ const colors = {
27
27
  dim: '\x1b[2m'
28
28
  };
29
29
 
30
+ async function resolveTemplateName(args) {
31
+ const templateIdx = args.indexOf('--template');
32
+ if (templateIdx > -1) {
33
+ return args[templateIdx + 1] || 'starter';
34
+ }
35
+
36
+ if (!process.stdin.isTTY || !process.stdout.isTTY || process.env.CI) {
37
+ return 'starter';
38
+ }
39
+
40
+ const options = [
41
+ { value: 'starter', label: 'starter', description: 'General-purpose starter' },
42
+ { value: 'starter-fintech', label: 'fintech', description: 'Payments, wallets, and compliance' },
43
+ { value: 'starter-website', label: 'website', description: 'Marketing and content sites' },
44
+ { value: 'starter-dashboard', label: 'dashboard', description: 'Admin panels and analytics' },
45
+ { value: 'starter-tailwind', label: 'tailwind', description: 'Tailwind-enabled starter' },
46
+ { value: 'starter-react', label: 'react', description: 'React-style starter' },
47
+ { value: 'starter-3d', label: '3d', description: '3D and immersive starter' }
48
+ ];
49
+
50
+ console.log('\nChoose a starter template:');
51
+ options.forEach((option, index) => {
52
+ console.log(` ${index + 1}. ${option.label} - ${option.description}`);
53
+ });
54
+
55
+ process.stdout.write('Enter a number [1-7] (default: 1): ');
56
+
57
+ const answer = await new Promise((resolve) => {
58
+ process.stdin.setEncoding('utf8');
59
+ process.stdin.once('data', (data) => resolve(data.toString().trim()));
60
+ });
61
+
62
+ const selection = Number(answer);
63
+ return options[selection - 1]?.value || 'starter';
64
+ }
65
+
30
66
  export async function createProject(targetName) {
31
67
  const projectName = targetName || 'africode-app';
32
68
 
@@ -50,22 +86,26 @@ ${colors.gold}╔═════════════════════
50
86
  await mkdir(projectDir);
51
87
 
52
88
  // 2. Copy Template Files
53
- // Support --template arg
54
- // node bin/create-africode.js my-app --template 3d
55
- const args = process.argv;
56
- const templateIdx = args.indexOf('--template');
57
- const templateName = templateIdx > -1 ? args[templateIdx + 1] : 'starter';
89
+ const args = process.argv.slice(2);
90
+ const templateName = await resolveTemplateName(args);
58
91
 
59
92
  // Validate template
60
- const validTemplates = ['starter', 'starter-3d', 'starter-tailwind', 'starter-react', '3d', 'tailwind', 'react'];
93
+ const validTemplates = ['starter', 'starter-3d', 'starter-tailwind', 'starter-react', 'starter-fintech', 'starter-website', 'starter-dashboard', '3d', 'tailwind', 'react', 'fintech', 'website', 'dashboard'];
61
94
  // Map short aliases to full template directory names
62
- const aliasMap = { '3d': 'starter-3d', 'tailwind': 'starter-tailwind', 'react': 'starter-react' };
95
+ const aliasMap = {
96
+ '3d': 'starter-3d',
97
+ 'tailwind': 'starter-tailwind',
98
+ 'react': 'starter-react',
99
+ 'fintech': 'starter-fintech',
100
+ 'website': 'starter-website',
101
+ 'dashboard': 'starter-dashboard'
102
+ };
63
103
  const actualTemplate = aliasMap[templateName] || templateName || 'starter';
64
104
 
65
105
  const templateDir = path.join(FRAMEWORK_ROOT, 'templates', actualTemplate);
66
106
 
67
107
  if (!existsSync(templateDir)) {
68
- throw new Error(`Template "${actualTemplate}" not found. Available: starter, tailwind, react, 3d`);
108
+ throw new Error(`Template "${actualTemplate}" not found. Available: starter, tailwind, react, fintech, website, dashboard, 3d`);
69
109
  }
70
110
 
71
111
  console.log(`${colors.blue}ℹ Using template: ${actualTemplate}${colors.reset}`);
@@ -84,17 +124,39 @@ ${colors.gold}╔═════════════════════
84
124
 
85
125
  await writeFile(pkgPath, JSON.stringify(pkgJson, null, 2));
86
126
 
87
- // 4. Create bin directory and copy CLI tool (africode.js)
88
- // This allows 'bun run bin/africode.js dev' to work in the new project
89
- const binSrc = path.join(FRAMEWORK_ROOT, 'bin', 'africode.js');
127
+ // 4. Create bin directory and provide a lightweight CLI wrapper
128
+ // that resolves the installed @africode/core package entrypoint.
90
129
  const binDestDir = path.join(projectDir, 'bin');
91
130
  await mkdir(binDestDir, { recursive: true });
92
131
 
93
- if (existsSync(binSrc)) {
94
- await cp(binSrc, path.join(binDestDir, 'africode.js'));
95
- } else {
96
- console.error(`${colors.red}✗ Critical: CLI entry point not found${colors.reset}`);
97
- }
132
+ const cliWrapper = `#!/usr/bin/env bun
133
+ import { spawnSync } from 'node:child_process';
134
+ import { existsSync } from 'node:fs';
135
+ import { fileURLToPath } from 'node:url';
136
+ import path from 'node:path';
137
+
138
+ const args = process.argv.slice(2);
139
+ const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../node_modules/@africode/core/bin/africode.js');
140
+
141
+ if (!existsSync(cliPath)) {
142
+ console.error("AfriCode CLI was not found in node_modules. Run 'bun install' first.");
143
+ process.exit(1);
144
+ }
145
+
146
+ const result = spawnSync('bun', [cliPath, ...args], {
147
+ stdio: 'inherit',
148
+ shell: false
149
+ });
150
+
151
+ if (result.error) {
152
+ console.error(result.error);
153
+ process.exit(1);
154
+ }
155
+
156
+ process.exit(result.status ?? 0);
157
+ `;
158
+
159
+ await writeFile(path.join(binDestDir, 'africode.js'), cliWrapper);
98
160
 
99
161
  // 5. Auto-generate index.js if missing
100
162
  const srcDir = path.join(projectDir, 'src');
@@ -110,8 +172,7 @@ ${colors.gold}╔═════════════════════
110
172
  * - SPA: import and initialize your router here
111
173
  */
112
174
 
113
- import { Layout } from './core/server/render.js';
114
- import { html } from './core/html.js';
175
+ import { Layout, html } from '@africode/core';
115
176
 
116
177
  // Example: Simple HTML page export
117
178
  export default function App() {
@@ -135,15 +196,16 @@ export default function App() {
135
196
  console.log(`${colors.green}✓ Project created successfully!${colors.reset}\n`);
136
197
  console.log(`${colors.bold}Your project contains:${colors.reset}`);
137
198
  console.log(` ${colors.blue}africode.config.js${colors.reset} - Framework Configuration`);
138
- console.log(` ${colors.blue}src/components/${colors.reset} - 24+ UI Components`);
139
- console.log(` ${colors.blue}src/styles/${colors.reset} - Design Tokens`);
140
- console.log(` ${colors.blue}src/pages/${colors.reset} - Documentation & Demos`);
141
- console.log(` ${colors.blue}src/index.js${colors.reset} - App Entry Point (auto-generated)`);
199
+ console.log(` ${colors.blue}.env.example${colors.reset} - Environment variable guide`);
200
+ console.log(` ${colors.blue}src/pages/${colors.reset} - File-based routes`);
201
+ console.log(` ${colors.blue}src/styles/${colors.reset} - Theme and design tokens`);
202
+ console.log(` ${colors.blue}src/index.js${colors.reset} - App entry point`);
203
+ console.log(` ${colors.blue}bin/africode.js${colors.reset} - Local CLI wrapper`);
142
204
  console.log(`\n${colors.bold}Next steps:${colors.reset}`);
143
205
  console.log(` ${colors.dim}cd${colors.reset} ${projectName}`);
144
- console.log(` ${colors.dim}bun install${colors.reset} (if needed)`);
145
- console.log(` ${colors.dim}bun run${colors.reset} dev\n`);
146
- console.log(`${colors.gold}Happy coding with the rhythm of the continent!${colors.reset}\n`);
206
+ console.log(` ${colors.dim}bun install${colors.reset}`);
207
+ console.log(` ${colors.dim}bun run ./bin/africode.js dev${colors.reset}`);
208
+ console.log(`\n${colors.gold}Happy coding with the rhythm of the continent!${colors.reset}\n`);
147
209
 
148
210
  } catch (err) {
149
211
  console.error(`${colors.red}✗ Failed to create project:${colors.reset}`, err);
@@ -0,0 +1,154 @@
1
+ import { AfriCodeComponent, registerComponent } from './base.js';
2
+
3
+ const styles = `
4
+ :host {
5
+ display: block;
6
+ color: var(--af-smart-text, var(--af-text, #172033));
7
+ font-family: var(--af-font-family, Inter, system-ui, sans-serif);
8
+ }
9
+
10
+ form {
11
+ display: grid;
12
+ gap: 12px;
13
+ }
14
+
15
+ label {
16
+ display: grid;
17
+ gap: 6px;
18
+ color: var(--af-smart-muted, var(--af-muted, #647084));
19
+ font-size: 0.86rem;
20
+ }
21
+
22
+ input {
23
+ min-height: 42px;
24
+ border: 1px solid var(--af-smart-border, var(--af-border, #d8dee8));
25
+ border-radius: 6px;
26
+ padding: 0 12px;
27
+ background: var(--af-smart-field-bg, var(--af-surface-muted, #eef2f7));
28
+ color: inherit;
29
+ }
30
+
31
+ button {
32
+ min-height: 42px;
33
+ border: 1px solid var(--af-smart-primary, var(--af-primary, #0f766e));
34
+ border-radius: 6px;
35
+ background: var(--af-smart-primary, var(--af-primary, #0f766e));
36
+ color: var(--af-smart-primary-text, #ffffff);
37
+ cursor: pointer;
38
+ font: inherit;
39
+ font-weight: 700;
40
+ }
41
+
42
+ button:disabled {
43
+ cursor: wait;
44
+ opacity: 0.7;
45
+ }
46
+
47
+ .message {
48
+ min-height: 20px;
49
+ color: var(--af-smart-muted, var(--af-muted, #647084));
50
+ font-size: 0.86rem;
51
+ }
52
+
53
+ .message.error {
54
+ color: var(--af-smart-danger, var(--af-danger, #b42318));
55
+ }
56
+
57
+ .message.success {
58
+ color: var(--af-smart-success, var(--af-success, #087443));
59
+ }
60
+ `;
61
+
62
+ function isEmail(value) {
63
+ return typeof value === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
64
+ }
65
+
66
+ export class AfriAuthForm extends AfriCodeComponent {
67
+ static get observedAttributes() {
68
+ return ['endpoint', 'button-label'];
69
+ }
70
+
71
+ connectedCallback() {
72
+ this.render();
73
+ }
74
+
75
+ attributeChangedCallback() {
76
+ if (this.isConnected) {
77
+ this.render();
78
+ }
79
+ }
80
+
81
+ get endpoint() {
82
+ return this.getAttribute('endpoint') || '/api/auth';
83
+ }
84
+
85
+ render() {
86
+ this.shadowRoot.innerHTML = `
87
+ <style>${styles}</style>
88
+ <form novalidate>
89
+ <label>
90
+ Email
91
+ <input name="email" type="email" autocomplete="email" required />
92
+ </label>
93
+ <label>
94
+ Password
95
+ <input name="password" type="password" autocomplete="current-password" minlength="8" required />
96
+ </label>
97
+ <button type="submit">${this.getAttribute('button-label') || 'Continue'}</button>
98
+ <div class="message" role="status" aria-live="polite"></div>
99
+ </form>
100
+ `;
101
+
102
+ this.shadowRoot.querySelector('form').addEventListener('submit', (event) => this.handleSubmit(event));
103
+ }
104
+
105
+ setMessage(message, type = '') {
106
+ const target = this.shadowRoot.querySelector('.message');
107
+ target.className = type ? `message ${type}` : 'message';
108
+ target.textContent = message;
109
+ }
110
+
111
+ async handleSubmit(event) {
112
+ event.preventDefault();
113
+ const form = event.currentTarget;
114
+ const button = form.querySelector('button');
115
+ const payload = {
116
+ email: form.elements.email.value.trim(),
117
+ password: form.elements.password.value
118
+ };
119
+
120
+ if (!isEmail(payload.email) || payload.password.length < 8) {
121
+ this.setMessage('Enter a valid email and password.', 'error');
122
+ this.emit('af-auth-error', { code: 'INVALID_INPUT' });
123
+ return;
124
+ }
125
+
126
+ button.disabled = true;
127
+ this.setMessage('Checking credentials...');
128
+
129
+ try {
130
+ const response = await fetch(this.endpoint, {
131
+ method: 'POST',
132
+ headers: { 'Content-Type': 'application/json' },
133
+ body: JSON.stringify(payload)
134
+ });
135
+ const data = await response.json();
136
+
137
+ if (!response.ok || data.ok === false) {
138
+ this.setMessage(data.error || 'Authentication failed.', 'error');
139
+ this.emit('af-auth-error', { response: data, status: response.status });
140
+ return;
141
+ }
142
+
143
+ this.setMessage('Authenticated.', 'success');
144
+ this.emit('af-auth-success', data);
145
+ } catch (error) {
146
+ this.setMessage('Authentication service unavailable.', 'error');
147
+ this.emit('af-auth-error', { code: 'NETWORK_ERROR', message: error.message });
148
+ } finally {
149
+ button.disabled = false;
150
+ }
151
+ }
152
+ }
153
+
154
+ registerComponent('af-auth-form', AfriAuthForm);
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Component Registry (Islands Map)
3
3
  *
4
+ * All AfriCode components, including the native UI kit (`af-ui-*`),
5
+ * dashboard widgets (`af-dashboard-*`), and website/system helpers,
6
+ * ship together from the same @africode/core package.
7
+ * This keeps theme, styles, registry, and versioning unified.
8
+ *
4
9
  * Instead of statically importing all components (monolithic bundle),
5
10
  * we export a map for lazy hydration.
6
11
  */
@@ -35,6 +40,11 @@ export { AfriDashboardTopbar } from './dashboard-topbar.js';
35
40
  export { AfriDashboardCard } from './dashboard-card.js';
36
41
  export { AfriDashboardMetric } from './dashboard-metric.js';
37
42
  export { AfriDashboardActivityList } from './dashboard-activity-list.js';
43
+ export { AfriAuthForm } from './auth-form.js';
44
+ export { AfriKycUpload } from './kyc-upload.js';
45
+ export { AfriWalletBalance } from './wallet-balance.js';
46
+ export { AfriTransactionLedger } from './transaction-ledger.js';
47
+ export { AfriNavDrawer } from './nav-drawer.js';
38
48
 
39
49
  // Map of Tag Name -> Dynamic Import
40
50
  export const componentMap = {
@@ -105,6 +115,11 @@ export const componentMap = {
105
115
 
106
116
  // Auth
107
117
  'af-auth': () => import('./auth.js'),
118
+ 'af-auth-form': () => import('./auth-form.js'),
119
+ 'af-kyc-upload': () => import('./kyc-upload.js'),
120
+ 'af-wallet-balance': () => import('./wallet-balance.js'),
121
+ 'af-transaction-ledger': () => import('./transaction-ledger.js'),
122
+ 'af-nav-drawer': () => import('./nav-drawer.js'),
108
123
 
109
124
  // Theming
110
125
  'af-theme-toggle': () => import('./theme-toggle.js'),
@@ -0,0 +1,173 @@
1
+ import { AfriCodeComponent, registerComponent } from './base.js';
2
+
3
+ const styles = `
4
+ :host {
5
+ display: block;
6
+ color: var(--af-smart-text, var(--af-text, #172033));
7
+ font-family: var(--af-font-family, Inter, system-ui, sans-serif);
8
+ }
9
+
10
+ form {
11
+ display: grid;
12
+ gap: 12px;
13
+ padding: 16px;
14
+ border: 1px solid var(--af-smart-border, var(--af-border, #d8dee8));
15
+ border-radius: 8px;
16
+ background: var(--af-smart-surface, var(--af-surface, #ffffff));
17
+ }
18
+
19
+ label {
20
+ display: grid;
21
+ gap: 6px;
22
+ color: var(--af-smart-muted, var(--af-muted, #647084));
23
+ font-size: 0.86rem;
24
+ }
25
+
26
+ input {
27
+ width: 100%;
28
+ border: 1px solid var(--af-smart-border, var(--af-border, #d8dee8));
29
+ border-radius: 6px;
30
+ padding: 10px;
31
+ background: var(--af-smart-field-bg, var(--af-surface-muted, #eef2f7));
32
+ color: inherit;
33
+ }
34
+
35
+ button {
36
+ min-height: 42px;
37
+ border: 1px solid var(--af-smart-primary, var(--af-primary, #0f766e));
38
+ border-radius: 6px;
39
+ background: var(--af-smart-primary, var(--af-primary, #0f766e));
40
+ color: var(--af-smart-primary-text, #ffffff);
41
+ cursor: pointer;
42
+ font: inherit;
43
+ font-weight: 700;
44
+ }
45
+
46
+ button:disabled {
47
+ cursor: wait;
48
+ opacity: 0.7;
49
+ }
50
+
51
+ .hint,
52
+ .message {
53
+ color: var(--af-smart-muted, var(--af-muted, #647084));
54
+ font-size: 0.84rem;
55
+ }
56
+
57
+ .message.error {
58
+ color: var(--af-smart-danger, var(--af-danger, #b42318));
59
+ }
60
+
61
+ .message.success {
62
+ color: var(--af-smart-success, var(--af-success, #087443));
63
+ }
64
+ `;
65
+
66
+ const allowedTypes = new Set(['application/pdf', 'image/jpeg', 'image/png']);
67
+
68
+ export class AfriKycUpload extends AfriCodeComponent {
69
+ static get observedAttributes() {
70
+ return ['endpoint', 'max-size-mb'];
71
+ }
72
+
73
+ connectedCallback() {
74
+ this.render();
75
+ }
76
+
77
+ attributeChangedCallback() {
78
+ if (this.isConnected) {
79
+ this.render();
80
+ }
81
+ }
82
+
83
+ get endpoint() {
84
+ return this.getAttribute('endpoint') || '/api/kyc';
85
+ }
86
+
87
+ get maxSizeMb() {
88
+ return Number(this.getAttribute('max-size-mb') || 5);
89
+ }
90
+
91
+ render() {
92
+ this.shadowRoot.innerHTML = `
93
+ <style>${styles}</style>
94
+ <form novalidate>
95
+ <label>
96
+ Identity document
97
+ <input name="document" type="file" accept="application/pdf,image/jpeg,image/png" required />
98
+ </label>
99
+ <span class="hint">PDF, JPG, or PNG. Maximum ${this.maxSizeMb} MB.</span>
100
+ <button type="submit">Upload for review</button>
101
+ <div class="message" role="status" aria-live="polite"></div>
102
+ </form>
103
+ `;
104
+
105
+ this.shadowRoot.querySelector('form').addEventListener('submit', (event) => this.handleSubmit(event));
106
+ }
107
+
108
+ setMessage(message, type = '') {
109
+ const target = this.shadowRoot.querySelector('.message');
110
+ target.className = type ? `message ${type}` : 'message';
111
+ target.textContent = message;
112
+ }
113
+
114
+ validateFile(file) {
115
+ if (!file) {
116
+ return { ok: false, code: 'FILE_REQUIRED', error: 'Choose an identity document.' };
117
+ }
118
+
119
+ if (!allowedTypes.has(file.type)) {
120
+ return { ok: false, code: 'UNSUPPORTED_FILE_TYPE', error: 'Use a PDF, JPG, or PNG document.' };
121
+ }
122
+
123
+ if (file.size > this.maxSizeMb * 1024 * 1024) {
124
+ return { ok: false, code: 'FILE_TOO_LARGE', error: `File must be ${this.maxSizeMb} MB or smaller.` };
125
+ }
126
+
127
+ return { ok: true };
128
+ }
129
+
130
+ async handleSubmit(event) {
131
+ event.preventDefault();
132
+ const form = event.currentTarget;
133
+ const button = form.querySelector('button');
134
+ const file = form.elements.document.files?.[0];
135
+ const validation = this.validateFile(file);
136
+
137
+ if (!validation.ok) {
138
+ this.setMessage(validation.error, 'error');
139
+ this.emit('af-kyc-error', validation);
140
+ return;
141
+ }
142
+
143
+ const payload = new FormData();
144
+ payload.set('document', file, file.name);
145
+
146
+ button.disabled = true;
147
+ this.setMessage('Uploading document...');
148
+
149
+ try {
150
+ const response = await fetch(this.endpoint, {
151
+ method: 'POST',
152
+ body: payload
153
+ });
154
+ const data = await response.json().catch(() => ({}));
155
+
156
+ if (!response.ok || data.ok === false) {
157
+ this.setMessage(data.error || 'Document upload failed.', 'error');
158
+ this.emit('af-kyc-error', { response: data, status: response.status });
159
+ return;
160
+ }
161
+
162
+ this.setMessage('Document received for review.', 'success');
163
+ this.emit('af-kyc-success', data);
164
+ } catch (error) {
165
+ this.setMessage('KYC service unavailable.', 'error');
166
+ this.emit('af-kyc-error', { code: 'NETWORK_ERROR', message: error.message });
167
+ } finally {
168
+ button.disabled = false;
169
+ }
170
+ }
171
+ }
172
+
173
+ registerComponent('af-kyc-upload', AfriKycUpload);