@visma-swno/customer-onboarding-wizard 1.0.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/README.md +233 -0
- package/dist/assets/Inter-Medium.woff2 +0 -0
- package/dist/assets/Inter-Regular.woff2 +0 -0
- package/dist/assets/Inter-SemiBold.woff2 +0 -0
- package/dist/assets/index.js +2315 -0
- package/dist/index.html +182 -0
- package/package.json +41 -0
package/dist/index.html
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>Customer Onboarding Wizard</title>
|
|
8
|
+
|
|
9
|
+
<style>
|
|
10
|
+
/* Load Inter font - Self-hosted */
|
|
11
|
+
@font-face {
|
|
12
|
+
font-family: 'Inter';
|
|
13
|
+
src: url('/assets/Inter-Regular.woff2') format('woff2');
|
|
14
|
+
font-weight: 400;
|
|
15
|
+
font-style: normal;
|
|
16
|
+
font-display: swap;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@font-face {
|
|
20
|
+
font-family: 'Inter';
|
|
21
|
+
src: url('/assets/Inter-Medium.woff2') format('woff2');
|
|
22
|
+
font-weight: 500;
|
|
23
|
+
font-style: normal;
|
|
24
|
+
font-display: swap;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@font-face {
|
|
28
|
+
font-family: 'Inter';
|
|
29
|
+
src: url('/assets/Inter-SemiBold.woff2') format('woff2');
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
font-style: normal;
|
|
32
|
+
font-display: swap;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Configure component font */
|
|
36
|
+
vsn-customer-onboarding-wizard {
|
|
37
|
+
--font-family: Inter, ui-sans-serif, system-ui, sans-serif;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Basic page styles */
|
|
41
|
+
body {
|
|
42
|
+
font-family: 'Inter', sans-serif;
|
|
43
|
+
margin: 0;
|
|
44
|
+
padding: 0;
|
|
45
|
+
}
|
|
46
|
+
</style>
|
|
47
|
+
<script type="module" crossorigin src="/assets/index.js"></script>
|
|
48
|
+
</head>
|
|
49
|
+
|
|
50
|
+
<body>
|
|
51
|
+
<!-- Your web components go here -->
|
|
52
|
+
<div id="app">
|
|
53
|
+
<vsn-customer-onboarding-wizard customerId="CUST-12345" baseUrl="http://localhost:3000" taskId="demo-task-12345"
|
|
54
|
+
locale="en-US" step="welcome" adminUrl="https://admin-user-access.stag.visma.net/context/customer/user-access">
|
|
55
|
+
</vsn-customer-onboarding-wizard>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<script>
|
|
59
|
+
// ============================================
|
|
60
|
+
// AUTHENTICATION TOKEN HANDLING
|
|
61
|
+
// ============================================
|
|
62
|
+
// CRITICAL: This listener MUST be set up BEFORE the component loads
|
|
63
|
+
// The component requests an authentication token via the 'request-token' event.
|
|
64
|
+
// The host application must listen for this event and provide the token.
|
|
65
|
+
//
|
|
66
|
+
// SECURITY: Follow OAuth2 best practices
|
|
67
|
+
// - Store tokens in sessionStorage (cleared on tab close)
|
|
68
|
+
// - NEVER log tokens to console in production
|
|
69
|
+
// - Use HTTPS in production environments
|
|
70
|
+
// - Implement proper error handling
|
|
71
|
+
|
|
72
|
+
// Example 1: Synchronous token retrieval from sessionStorage
|
|
73
|
+
// Uncomment to use:
|
|
74
|
+
/*
|
|
75
|
+
document.addEventListener('request-token', (event) => {
|
|
76
|
+
const token = sessionStorage.getItem('accessToken');
|
|
77
|
+
|
|
78
|
+
if (event.detail && typeof event.detail.callback === 'function') {
|
|
79
|
+
event.detail.callback(token);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
// Example 2: Asynchronous token retrieval from server
|
|
85
|
+
// Currently active for demonstration
|
|
86
|
+
document.addEventListener('request-token', async (event) => {
|
|
87
|
+
try {
|
|
88
|
+
// Using Vite proxy - requests to /v1/* are proxied to http://localhost:3000
|
|
89
|
+
// In production, configure your hosting to proxy API requests or use full API URL
|
|
90
|
+
const response = await fetch('/v1/me/token', {
|
|
91
|
+
headers: {
|
|
92
|
+
'Accept': 'application/json'
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
throw new Error(`Token retrieval failed: ${response.status}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const data = await response.json();
|
|
101
|
+
|
|
102
|
+
// Validate response structure
|
|
103
|
+
if (!data?.accessToken) {
|
|
104
|
+
throw new Error('Invalid token response format');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (event.detail && typeof event.detail.callback === 'function') {
|
|
108
|
+
event.detail.callback(data.accessToken);
|
|
109
|
+
}
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error('Failed to retrieve authentication token');
|
|
112
|
+
|
|
113
|
+
if (event.detail && typeof event.detail.callback === 'function') {
|
|
114
|
+
event.detail.callback(null);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<script>
|
|
121
|
+
// ============================================
|
|
122
|
+
// HTTP ERROR HANDLING
|
|
123
|
+
// ============================================
|
|
124
|
+
// The component emits 'http-error' events ONLY for authentication/authorization errors:
|
|
125
|
+
// - 401 Unauthorized: Token expired/invalid
|
|
126
|
+
// - 403 Forbidden: Insufficient permissions
|
|
127
|
+
//
|
|
128
|
+
// Other errors (400, 422, 500, network errors) are automatically displayed
|
|
129
|
+
// by the component via an internal error banner. No handling required by host app.
|
|
130
|
+
//
|
|
131
|
+
// Host application responsibilities:
|
|
132
|
+
// - Handle 401: Refresh token and retry request
|
|
133
|
+
// - Handle 403: Redirect to access request page or show permission denied
|
|
134
|
+
|
|
135
|
+
document.addEventListener('http-error', async (event) => {
|
|
136
|
+
const { status, statusText, endpoint, retry } = event.detail;
|
|
137
|
+
|
|
138
|
+
console.log(`Auth Error: ${status} ${statusText}`, { endpoint });
|
|
139
|
+
|
|
140
|
+
// Handle 401 Unauthorized - refresh token and retry
|
|
141
|
+
if (status === 401) {
|
|
142
|
+
console.log('Token expired. Attempting to refresh...');
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Fetch new token
|
|
146
|
+
const response = await fetch('/v1/me/token', {
|
|
147
|
+
headers: { 'Accept': 'application/json' }
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (response.ok) {
|
|
151
|
+
const data = await response.json();
|
|
152
|
+
|
|
153
|
+
// Update token in component
|
|
154
|
+
const wizard = document.querySelector('vsn-customer-onboarding-wizard');
|
|
155
|
+
if (wizard && wizard.updateToken) {
|
|
156
|
+
wizard.updateToken(data.accessToken);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log('Token refreshed. Retrying request...');
|
|
160
|
+
|
|
161
|
+
// Retry the failed request with new token
|
|
162
|
+
await retry();
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error('Failed to refresh token. Redirecting to login...');
|
|
166
|
+
// In production: redirect to login page
|
|
167
|
+
// window.location.href = '/login';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Handle 403 Forbidden
|
|
172
|
+
if (status === 403) {
|
|
173
|
+
console.error('Permission denied:', endpoint);
|
|
174
|
+
// In production: redirect or show modal
|
|
175
|
+
// window.location.href = '/request-access';
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
</script>
|
|
179
|
+
|
|
180
|
+
</body>
|
|
181
|
+
|
|
182
|
+
</html>
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@visma-swno/customer-onboarding-wizard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "dist/assets/index.js",
|
|
6
|
+
"types": "dist/assets/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist/**/*"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "vite --open",
|
|
12
|
+
"build": "tsc && vite build --emptyOutDir",
|
|
13
|
+
"preview": "vite preview",
|
|
14
|
+
"mock:server": "node server.js",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"test:coverage": "vitest run --coverage",
|
|
18
|
+
"generate:tokens": "node scripts/generate-tokens.mjs"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@lit/task": "^1.0.0",
|
|
22
|
+
"lit": "^3.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@lit/task": "^1.0.2",
|
|
26
|
+
"@testing-library/dom": "^10.4.0",
|
|
27
|
+
"@vitest/coverage-v8": "^3.1.4",
|
|
28
|
+
"@vsn-ux/gaia-styles": "^0.6.6",
|
|
29
|
+
"cors": "^2.8.5",
|
|
30
|
+
"express": "^4.21.2",
|
|
31
|
+
"happy-dom": "^20.8.9",
|
|
32
|
+
"jsdom": "^26.1.0",
|
|
33
|
+
"lit": "^3.2.1",
|
|
34
|
+
"typescript": "~5.7.2",
|
|
35
|
+
"vite": "^6.1.0",
|
|
36
|
+
"vitest": "^3.1.4"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|