@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.
- package/AGENTS.md +2 -0
- package/AGENT_INSTRUCTIONS.md +595 -595
- package/COMPONENT_SCHEMA.json +1800 -990
- package/README.md +2 -0
- package/bin/create-africode.js +87 -25
- package/components/auth-form.js +154 -0
- package/components/index.js +15 -0
- package/components/kyc-upload.js +173 -0
- package/components/nav-drawer.js +217 -0
- package/components/transaction-ledger.js +138 -0
- package/components/wallet-balance.js +114 -0
- package/core/a2ui-schema-manager.js +1 -1
- package/core/a2ui.js +178 -1
- package/core/bun-runtime.js +122 -7
- package/core/cli/commands/build.js +30 -5
- package/core/cli/ui.js +13 -3
- package/core/compliance.js +201 -0
- package/core/middleware.js +80 -17
- package/core/patterns.js +168 -0
- package/core/request-analytics.js +254 -0
- package/core/request-identity.js +79 -29
- package/core/sdk.js +4 -1
- package/core/validation.js +13 -0
- package/dist/africode.js +858 -457
- package/dist/africode.js.map +14 -9
- package/dist/build-info.json +3 -3
- package/dist/components.js +784 -383
- package/dist/components.js.map +11 -6
- package/package.json +1 -1
- package/templates/starter/package.json +5 -5
- package/templates/starter/src/index.js +18 -0
- package/templates/starter/src/pages/index.js +1 -1
- package/templates/starter-3d/package.json +5 -5
- package/templates/starter-3d/src/pages/index.js +1 -1
- package/templates/starter-dashboard/.env.example +21 -0
- package/templates/starter-dashboard/africode.config.js +20 -0
- package/templates/starter-dashboard/package.json +14 -0
- package/templates/starter-dashboard/src/index.js +17 -0
- package/templates/starter-dashboard/src/pages/api/analytics.js +24 -0
- package/templates/starter-dashboard/src/pages/index.html +118 -0
- package/templates/starter-dashboard/src/pages/index.js +110 -0
- package/templates/starter-dashboard/src/styles/main.css +172 -0
- package/templates/starter-fintech/.env.example +28 -0
- package/templates/starter-fintech/africode.config.js +20 -0
- package/templates/starter-fintech/package.json +15 -0
- package/templates/starter-fintech/src/index.js +17 -0
- package/templates/starter-fintech/src/pages/api/auth.js +45 -0
- package/templates/starter-fintech/src/pages/api/transfer.js +65 -0
- package/templates/starter-fintech/src/pages/api/wallet.js +39 -0
- package/templates/starter-fintech/src/pages/api/webhooks/payment.js +32 -0
- package/templates/starter-fintech/src/pages/index.html +169 -0
- package/templates/starter-fintech/src/pages/index.js +161 -0
- package/templates/starter-fintech/src/styles/main.css +246 -0
- package/templates/starter-react/package.json +5 -5
- package/templates/starter-react/src/pages/index.js +1 -1
- package/templates/starter-tailwind/package.json +5 -5
- package/templates/starter-tailwind/src/pages/index.js +1 -1
- package/templates/starter-website/.env.example +18 -0
- package/templates/starter-website/africode.config.js +20 -0
- package/templates/starter-website/package.json +14 -0
- package/templates/starter-website/src/index.js +17 -0
- package/templates/starter-website/src/pages/index.html +124 -0
- package/templates/starter-website/src/pages/index.js +116 -0
- 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:
|
package/bin/create-africode.js
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
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 = {
|
|
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
|
|
88
|
-
//
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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 '
|
|
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}
|
|
139
|
-
console.log(` ${colors.blue}src/
|
|
140
|
-
console.log(` ${colors.blue}src/
|
|
141
|
-
console.log(` ${colors.blue}src/index.js${colors.reset} - App
|
|
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}
|
|
145
|
-
console.log(` ${colors.dim}bun run${colors.reset}
|
|
146
|
-
console.log(
|
|
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);
|
package/components/index.js
CHANGED
|
@@ -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);
|