summit-registration-lite 7.0.5 → 7.0.7
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/dist/components/index.js +85 -32
- package/dist/components/index.js.map +1 -1
- package/dist/components/registration-form.js +85 -32
- package/dist/components/registration-form.js.map +1 -1
- package/dist/components/registration-modal.js +85 -32
- package/dist/components/registration-modal.js.map +1 -1
- package/dist/index.js +85 -32
- package/dist/index.js.map +1 -1
- package/e2e/promo-code-date-filter.spec.js +103 -0
- package/e2e/promo-code-discovery.spec.js +55 -7
- package/e2e/promo-code-invalid-clears-warning.spec.js +57 -0
- package/package.json +3 -3
- package/.claude/rules/summit-registration-lite-component-props.md +0 -95
- package/.claude/rules/summit-registration-lite-i18n.md +0 -62
- package/.claude/rules/summit-registration-lite-payment-providers.md +0 -69
- package/.claude/rules/summit-registration-lite-project.md +0 -80
- package/.claude/rules/summit-registration-lite-redux-actions.md +0 -65
- package/.claude/rules/summit-registration-lite-step-flow.md +0 -77
- package/.claude/rules/summit-registration-lite-testing.md +0 -97
- package/.claude/rules/summit-registration-lite-utils.md +0 -71
- package/.claude/skills/summit-registration-lite-add-provider/SKILL.md +0 -155
- package/.claude/skills/summit-registration-lite-dev-setup/SKILL.md +0 -67
- package/.claude/skills/summit-registration-lite-publish/SKILL.md +0 -64
- package/.claude/skills/summit-registration-lite-scaffold-component/SKILL.md +0 -152
- package/.codegraph/config.json +0 -141
- package/.codegraph/daemon.pid +0 -6
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const { test, expect } = require('@playwright/test');
|
|
2
|
+
const {
|
|
3
|
+
ticketType,
|
|
4
|
+
discoveredCode,
|
|
5
|
+
discoveryResponse,
|
|
6
|
+
ticketTypesResponse,
|
|
7
|
+
taxTypesResponse,
|
|
8
|
+
validationResponse,
|
|
9
|
+
} = require('./fixtures');
|
|
10
|
+
|
|
11
|
+
// Post-apply auto-select must scan the date-filtered ticket list (`allowedTicketTypes`),
|
|
12
|
+
// not the raw Redux list (`originalTicketTypes`). Otherwise the widget could pick a ticket
|
|
13
|
+
// that's outside its sales window — the dropdown can't display it and the user is stuck.
|
|
14
|
+
|
|
15
|
+
const setupRoutes = async (page, { discovery, tickets, validation }) => {
|
|
16
|
+
await page.route('**/promo-codes/all/discover*', route =>
|
|
17
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(discoveryResponse(discovery)) })
|
|
18
|
+
);
|
|
19
|
+
await page.route('**/ticket-types/allowed*', route =>
|
|
20
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(ticketTypesResponse(tickets)) })
|
|
21
|
+
);
|
|
22
|
+
await page.route('**/tax-types*', route =>
|
|
23
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(taxTypesResponse()) })
|
|
24
|
+
);
|
|
25
|
+
if (validation) {
|
|
26
|
+
await page.route('**/promo-codes/*/apply*', route =>
|
|
27
|
+
route.fulfill({ status: validation.status, contentType: 'application/json', body: JSON.stringify(validation.body) })
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
test.describe('post-apply auto-select respects sales window', () => {
|
|
33
|
+
test('picks the in-window ticket when a discovered code matches both an expired and an active ticket', async ({ page }) => {
|
|
34
|
+
// Code matches both tickets, but Expired Ticket has a sales_end_date in the past.
|
|
35
|
+
// Pre-fix, the auto-select scanned originalTicketTypes (unfiltered) and would land
|
|
36
|
+
// on Expired Ticket — first match in iteration order. Post-fix, only Active Ticket
|
|
37
|
+
// can be auto-selected because Expired Ticket is excluded by the date filter.
|
|
38
|
+
const expiredTicket = ticketType({
|
|
39
|
+
id: 188,
|
|
40
|
+
name: 'Expired Ticket',
|
|
41
|
+
sales_start_date: 1000000, // long ago
|
|
42
|
+
sales_end_date: 2000000, // also long ago
|
|
43
|
+
});
|
|
44
|
+
const activeTicket = ticketType({
|
|
45
|
+
id: 200,
|
|
46
|
+
name: 'Active Ticket',
|
|
47
|
+
sales_start_date: null,
|
|
48
|
+
sales_end_date: null,
|
|
49
|
+
});
|
|
50
|
+
const code = discoveredCode({
|
|
51
|
+
auto_apply: true,
|
|
52
|
+
allowed_ticket_types: [188, 200],
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await setupRoutes(page, {
|
|
56
|
+
tickets: [expiredTicket, activeTicket],
|
|
57
|
+
discovery: [code],
|
|
58
|
+
validation: { status: 200, body: validationResponse() },
|
|
59
|
+
});
|
|
60
|
+
await page.goto('/');
|
|
61
|
+
|
|
62
|
+
// Auto-apply runs on load; the post-apply effect picks a qualifying ticket.
|
|
63
|
+
// The dropdown's selected-ticket slot shows the chosen ticket's name.
|
|
64
|
+
await expect(page.locator('[data-testid="selected-ticket"]')).toContainText('Active Ticket');
|
|
65
|
+
await expect(page.locator('[data-testid="selected-ticket"]')).not.toContainText('Expired Ticket');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('never picks an expired ticket even when it is the only qualifying one', async ({ page }) => {
|
|
69
|
+
// Code matches only the expired ticket. Two active tickets exist (so the
|
|
70
|
+
// "fall back to the single allowed option" clause cannot mask the bug).
|
|
71
|
+
// Pre-fix, the widget scanned originalTicketTypes and would land on
|
|
72
|
+
// Expired Ticket — the only qualifying one. Post-fix, the qualifying
|
|
73
|
+
// scan is bounded by allowedTicketTypes, so Expired Ticket is never
|
|
74
|
+
// selectable.
|
|
75
|
+
const expiredTicket = ticketType({
|
|
76
|
+
id: 188,
|
|
77
|
+
name: 'Expired Ticket',
|
|
78
|
+
sales_start_date: 1000000,
|
|
79
|
+
sales_end_date: 2000000,
|
|
80
|
+
});
|
|
81
|
+
const activeA = ticketType({ id: 200, name: 'Active A', sales_start_date: null, sales_end_date: null });
|
|
82
|
+
const activeB = ticketType({ id: 201, name: 'Active B', sales_start_date: null, sales_end_date: null });
|
|
83
|
+
const code = discoveredCode({
|
|
84
|
+
auto_apply: true,
|
|
85
|
+
allowed_ticket_types: [188], // only the expired one qualifies
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await setupRoutes(page, {
|
|
89
|
+
tickets: [expiredTicket, activeA, activeB],
|
|
90
|
+
discovery: [code],
|
|
91
|
+
validation: { status: 200, body: validationResponse() },
|
|
92
|
+
});
|
|
93
|
+
await page.goto('/');
|
|
94
|
+
|
|
95
|
+
// Give the auto-apply + post-apply effects time to settle.
|
|
96
|
+
await page.waitForTimeout(500);
|
|
97
|
+
// Post-fix: no auto-selection happens because the only qualifying ticket
|
|
98
|
+
// is filtered out by date and the fallback ("if there's a single allowed
|
|
99
|
+
// option, pick it") cannot fire when more than one is allowed. The dropdown
|
|
100
|
+
// shows its placeholder. Pre-fix, this slot would have shown "Expired Ticket".
|
|
101
|
+
await expect(page.locator('[data-testid="no-ticket"]')).toBeVisible();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -124,6 +124,27 @@ test.describe('suggestion flow (auto_apply: false)', () => {
|
|
|
124
124
|
await page.fill('input[placeholder="Enter your promo code"]', 'SUGGEST50');
|
|
125
125
|
await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
test('banner swaps to "applies to a different ticket" copy after switching to a non-qualifying ticket', async ({ page }) => {
|
|
129
|
+
const qualifying = ticketType({ id: 188, name: 'Early Bird Ticket' });
|
|
130
|
+
const nonQualifying = ticketType({ id: 999, name: 'Standard Ticket' });
|
|
131
|
+
const code = discoveredCode({ auto_apply: false, code: 'SUGGEST50', allowed_ticket_types: [188] });
|
|
132
|
+
|
|
133
|
+
await setupRoutes(page, {
|
|
134
|
+
tickets: [qualifying, nonQualifying],
|
|
135
|
+
discovery: [code],
|
|
136
|
+
});
|
|
137
|
+
await page.goto('/');
|
|
138
|
+
|
|
139
|
+
await selectTicket(page, 'Early Bird Ticket');
|
|
140
|
+
await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
|
|
141
|
+
|
|
142
|
+
await selectTicket(page, 'Standard Ticket');
|
|
143
|
+
await expect(page.locator('text=Following promo code applies to a different ticket. Apply to switch.')).toBeVisible();
|
|
144
|
+
// The personal "You qualify" copy must be gone — banner stays visible
|
|
145
|
+
// but accurate about the current pick.
|
|
146
|
+
await expect(page.locator('text=You qualify for the following promo code:')).toHaveCount(0);
|
|
147
|
+
});
|
|
127
148
|
});
|
|
128
149
|
|
|
129
150
|
test.describe('auto-apply flow (auto_apply: true)', () => {
|
|
@@ -154,15 +175,29 @@ test.describe('auto-apply flow (auto_apply: true)', () => {
|
|
|
154
175
|
await expect(page.locator('text=You qualify for the following promo code:')).toBeVisible();
|
|
155
176
|
});
|
|
156
177
|
|
|
157
|
-
test('
|
|
178
|
+
test('auto-applies on load and surfaces INVALID when the only ticket is non-qualifying', async ({ page }) => {
|
|
158
179
|
const nonQualifyingTicket = ticketType({ id: 999, name: 'Standard Ticket' });
|
|
159
180
|
await setupRoutes(page, {
|
|
160
181
|
tickets: [nonQualifyingTicket],
|
|
161
182
|
discovery: [autoCode],
|
|
183
|
+
// Per the SDS, early auto-apply fires whenever a single auto_apply code
|
|
184
|
+
// exists — independent of which tickets are present. Re-validation against
|
|
185
|
+
// the lone non-qualifying ticket then catches the mismatch and the user
|
|
186
|
+
// sees the backend's rejection message.
|
|
187
|
+
validation: (route) => {
|
|
188
|
+
if (route.request().url().includes('ticket_type_id%3D%3D999')) {
|
|
189
|
+
return route.fulfill({
|
|
190
|
+
status: 412,
|
|
191
|
+
contentType: 'application/json',
|
|
192
|
+
body: JSON.stringify(validationError('Promo code AUTO100 can not be applied to Ticket Type Standard Ticket.')),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(validationResponse()) });
|
|
196
|
+
},
|
|
162
197
|
});
|
|
163
198
|
await page.goto('/');
|
|
164
|
-
await
|
|
165
|
-
await expect(page.locator('text=
|
|
199
|
+
await expect(page.locator('input[placeholder="Enter your promo code"]')).toHaveValue('AUTO100');
|
|
200
|
+
await expect(page.locator('text=Promo code AUTO100 can not be applied to Ticket Type Standard Ticket.')).toBeVisible();
|
|
166
201
|
});
|
|
167
202
|
});
|
|
168
203
|
|
|
@@ -288,7 +323,7 @@ test.describe('no tickets available', () => {
|
|
|
288
323
|
});
|
|
289
324
|
|
|
290
325
|
test.describe('ticket switching with applied code', () => {
|
|
291
|
-
test('
|
|
326
|
+
test('keeps code applied and surfaces INVALID when switching to non-qualifying ticket', async ({ page }) => {
|
|
292
327
|
const qualifying = ticketType({ id: 188, name: 'Early Bird Ticket' });
|
|
293
328
|
const nonQualifying = ticketType({ id: 999, name: 'Standard Ticket' });
|
|
294
329
|
const code = discoveredCode({ auto_apply: true, allowed_ticket_types: [188] });
|
|
@@ -296,15 +331,28 @@ test.describe('ticket switching with applied code', () => {
|
|
|
296
331
|
await setupRoutes(page, {
|
|
297
332
|
tickets: [qualifying, nonQualifying],
|
|
298
333
|
discovery: [code],
|
|
299
|
-
|
|
334
|
+
// Reject the validate call when targeting the non-qualifying ticket type.
|
|
335
|
+
validation: (route) => {
|
|
336
|
+
if (route.request().url().includes('ticket_type_id%3D%3D999')) {
|
|
337
|
+
return route.fulfill({
|
|
338
|
+
status: 412,
|
|
339
|
+
contentType: 'application/json',
|
|
340
|
+
body: JSON.stringify(validationError('Promo code EARLYBIRD can not be applied to Ticket Type Standard Ticket.')),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
return route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(validationResponse()) });
|
|
344
|
+
},
|
|
300
345
|
});
|
|
301
346
|
await page.goto('/');
|
|
302
347
|
await selectTicket(page, 'Early Bird Ticket');
|
|
303
348
|
await expect(page.locator('text=Following promo code was automatically applied:')).toBeVisible();
|
|
304
349
|
|
|
305
350
|
await selectTicket(page, 'Standard Ticket');
|
|
306
|
-
|
|
307
|
-
|
|
351
|
+
// The code is no longer silently removed on a non-qualifying ticket switch;
|
|
352
|
+
// it stays applied and is re-validated, surfacing the backend rejection so
|
|
353
|
+
// the user can choose to Remove or pick another qualifying ticket.
|
|
354
|
+
await expect(page.locator('input[placeholder="Enter your promo code"]')).toHaveValue('EARLYBIRD');
|
|
355
|
+
await expect(page.locator('text=Promo code EARLYBIRD can not be applied to Ticket Type Standard Ticket.')).toBeVisible();
|
|
308
356
|
});
|
|
309
357
|
|
|
310
358
|
test('suggestion appears when switching back to qualifying ticket', async ({ page }) => {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { test, expect } = require('@playwright/test');
|
|
2
|
+
const {
|
|
3
|
+
ticketType,
|
|
4
|
+
discoveryResponse,
|
|
5
|
+
ticketTypesResponse,
|
|
6
|
+
taxTypesResponse,
|
|
7
|
+
validationError,
|
|
8
|
+
} = require('./fixtures');
|
|
9
|
+
|
|
10
|
+
// The unapplied-code warning ("you typed a code but didn't click Apply") must yield
|
|
11
|
+
// to the hook's own validation error once the backend rejects the code. Without the
|
|
12
|
+
// fix, the warning kept its precedence in the merged display slot, masking the
|
|
13
|
+
// more specific "Promo code entered is not valid" message after a failed Apply.
|
|
14
|
+
|
|
15
|
+
const UNAPPLIED_WARNING = "You entered a promo code but it hasn't been applied";
|
|
16
|
+
const INVALID_MESSAGE = 'Promo code entered is not valid.';
|
|
17
|
+
|
|
18
|
+
test('INVALID status clears the unapplied-code warning', async ({ page }) => {
|
|
19
|
+
await page.route('**/promo-codes/all/discover*', route =>
|
|
20
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(discoveryResponse([])) })
|
|
21
|
+
);
|
|
22
|
+
await page.route('**/ticket-types/allowed*', route =>
|
|
23
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(ticketTypesResponse([ticketType()])) })
|
|
24
|
+
);
|
|
25
|
+
await page.route('**/tax-types*', route =>
|
|
26
|
+
route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(taxTypesResponse()) })
|
|
27
|
+
);
|
|
28
|
+
await page.route('**/promo-codes/*/apply*', route =>
|
|
29
|
+
route.fulfill({
|
|
30
|
+
status: 412,
|
|
31
|
+
contentType: 'application/json',
|
|
32
|
+
body: JSON.stringify(validationError('The Promo Code "BAD" is not a valid code.')),
|
|
33
|
+
})
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
await page.goto('/');
|
|
37
|
+
|
|
38
|
+
// Select a ticket so the Next button is enabled.
|
|
39
|
+
await page.locator('[data-testid="ticket-dropdown"]').click();
|
|
40
|
+
await page.locator('[data-testid="ticket-list"] >> text=Early Bird Ticket').click();
|
|
41
|
+
|
|
42
|
+
// Type a code WITHOUT clicking Apply, then hit Next.
|
|
43
|
+
// This is the trigger for the unapplied-code warning.
|
|
44
|
+
await page.fill('input[placeholder="Enter your promo code"]', 'BAD');
|
|
45
|
+
await page.click('button:has-text("Next")');
|
|
46
|
+
|
|
47
|
+
// Warning is visible, INVALID message is not.
|
|
48
|
+
await expect(page.locator(`text=${UNAPPLIED_WARNING}`)).toBeVisible();
|
|
49
|
+
await expect(page.locator(`text=${INVALID_MESSAGE}`)).not.toBeVisible();
|
|
50
|
+
|
|
51
|
+
// Now click Apply. Backend rejects with 412 — the hook surfaces the INVALID message.
|
|
52
|
+
await page.click('button:has-text("Apply")');
|
|
53
|
+
|
|
54
|
+
// The unapplied warning must give way to the more specific rejection reason.
|
|
55
|
+
await expect(page.locator(`text=${INVALID_MESSAGE}`)).toBeVisible();
|
|
56
|
+
await expect(page.locator(`text=${UNAPPLIED_WARNING}`)).not.toBeVisible();
|
|
57
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "summit-registration-lite",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.7",
|
|
4
4
|
"description": "Summit Registration Lite",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
"mini-css-extract-plugin": "^2.6.0",
|
|
79
79
|
"moment": "^2.22.2",
|
|
80
80
|
"moment-timezone": "^0.5.21",
|
|
81
|
-
"openstack-uicore-foundation": "4.2.
|
|
81
|
+
"openstack-uicore-foundation": "4.2.32",
|
|
82
82
|
"optimize-css-assets-webpack-plugin": "^6.0.1",
|
|
83
83
|
"path": "^0.12.7",
|
|
84
84
|
"react": "^16.8.4",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
"lodash": "^4.17.14",
|
|
131
131
|
"moment": "^2.22.2",
|
|
132
132
|
"moment-timezone": "^0.5.21",
|
|
133
|
-
"openstack-uicore-foundation": "4.2.
|
|
133
|
+
"openstack-uicore-foundation": "4.2.32",
|
|
134
134
|
"react": "^16.8.4",
|
|
135
135
|
"react-bootstrap": "^0.31.5",
|
|
136
136
|
"react-datetime": "^2.16.2",
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
# Component Prop Conventions
|
|
2
|
-
|
|
3
|
-
## When to Apply
|
|
4
|
-
|
|
5
|
-
Defining props for new components or understanding prop flow from widget to child components.
|
|
6
|
-
|
|
7
|
-
## Pattern
|
|
8
|
-
|
|
9
|
-
**Widget Root (RegistrationLiteWidget):**
|
|
10
|
-
|
|
11
|
-
Parent app passes all props to widget, which wraps `<RegistrationLite>` in Redux Provider.
|
|
12
|
-
|
|
13
|
-
**Main Component (RegistrationLite):**
|
|
14
|
-
|
|
15
|
-
```javascript
|
|
16
|
-
const RegistrationLite = ({
|
|
17
|
-
// Lifecycle callbacks (parent app control)
|
|
18
|
-
authUser, // (provider) => void - handle SSO login
|
|
19
|
-
goToExtraQuestions, // (attendeeId) => void - redirect to extra questions
|
|
20
|
-
goToEvent, // () => void - redirect to event page
|
|
21
|
-
goToMyOrders, // () => void - redirect to my orders
|
|
22
|
-
onPurchaseComplete, // (order) => void - handle successful purchase
|
|
23
|
-
closeWidget, // () => void - close/minimize widget
|
|
24
|
-
|
|
25
|
-
// Auth callbacks
|
|
26
|
-
getPasswordlessCode, // (email) => void - request OTP code
|
|
27
|
-
loginWithCode, // (email, code) => void - verify OTP
|
|
28
|
-
authErrorCallback, // (error) => void - handle auth errors
|
|
29
|
-
|
|
30
|
-
// Error handling
|
|
31
|
-
onError, // ({type, msg, exception}) => void
|
|
32
|
-
handleCompanyError, // (error) => void - company dropdown errors
|
|
33
|
-
|
|
34
|
-
// Data
|
|
35
|
-
summitData, // { id, name, time_zone, registration_disclaimer, ... }
|
|
36
|
-
profileData, // { email, first_name, last_name, company, ... }
|
|
37
|
-
|
|
38
|
-
// Config
|
|
39
|
-
apiBaseUrl, // Base URL for API calls
|
|
40
|
-
supportEmail, // Support email for help text
|
|
41
|
-
allowPromoCodes, // boolean - show/hide promo code field (default: true)
|
|
42
|
-
showCompanyInput, // boolean - show/hide company field (default: true)
|
|
43
|
-
allowsNativeAuth, // boolean - show email/password login
|
|
44
|
-
allowsOtpAuth, // boolean - show passwordless OTP login
|
|
45
|
-
loginOptions, // [{ button_color, provider_label, provider_param }]
|
|
46
|
-
|
|
47
|
-
// UI customization
|
|
48
|
-
loading, // boolean - external loading state
|
|
49
|
-
showMultipleTicketTexts, // boolean - show multi-ticket messaging
|
|
50
|
-
hidePostalCode, // boolean - hide postal code in payment (default: false)
|
|
51
|
-
idpLogoDark, // string - custom logo src (dark theme)
|
|
52
|
-
idpLogoLight, // string - custom logo src (light theme)
|
|
53
|
-
|
|
54
|
-
// Messages (optional overrides)
|
|
55
|
-
noAllowedTicketsMessage,
|
|
56
|
-
ticketTaxesErrorMessage,
|
|
57
|
-
|
|
58
|
-
// Provider options
|
|
59
|
-
providerOptions, // { stripe: {...}, lawpay: {...} }
|
|
60
|
-
successfulPaymentReturnUrl, // Stripe return URL after payment
|
|
61
|
-
|
|
62
|
-
// Redux state (from connect)
|
|
63
|
-
step, reservation, ticketTypes, taxTypes, ...
|
|
64
|
-
|
|
65
|
-
// Redux actions (from connect)
|
|
66
|
-
loadSession, changeStep, reserveTicket, payTicketWithProvider, ...
|
|
67
|
-
}) => { /* ... */ }
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
## Key Points
|
|
71
|
-
|
|
72
|
-
- **Callback pattern:** Parent app controls navigation (`goToExtraQuestions`, `goToEvent`, etc.) - widget doesn't redirect directly
|
|
73
|
-
- **Error handling delegation:** Widget calls `onError` / `authErrorCallback` / `handleCompanyError`, parent decides what to do
|
|
74
|
-
- **Data vs. Config:** `summitData` is event info, `profileData` is user info, others are config/behavior flags
|
|
75
|
-
- **Optional overrides:** Most text/messages have defaults but can be overridden via props
|
|
76
|
-
- **Redux separation:** Connected props (`step`, `reservation`) vs. passed props (callbacks, config)
|
|
77
|
-
|
|
78
|
-
## Prop Flow Example
|
|
79
|
-
|
|
80
|
-
```
|
|
81
|
-
Parent App
|
|
82
|
-
└─> RegistrationLiteWidget (wraps Provider)
|
|
83
|
-
└─> RegistrationLite (receives props + Redux state)
|
|
84
|
-
├─> LoginComponent (loginOptions, allowsNativeAuth, allowsOtpAuth)
|
|
85
|
-
├─> TicketTypeComponent (ticketTypes, summitData)
|
|
86
|
-
├─> PersonalInfoComponent (profileData, showCompanyInput)
|
|
87
|
-
└─> PaymentComponent (reservation, providerOptions)
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
## Common Mistakes
|
|
91
|
-
|
|
92
|
-
- Hardcoding navigation instead of calling prop callbacks (breaks widget embedding)
|
|
93
|
-
- Not providing `apiBaseUrl` (causes API calls to fail)
|
|
94
|
-
- Forgetting `onError` handler (errors go to console only, user sees nothing)
|
|
95
|
-
- Passing incomplete `summitData` (missing `time_zone` breaks clock, missing `id` breaks API calls)
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# i18n / Translation Pattern
|
|
2
|
-
|
|
3
|
-
## When to Apply
|
|
4
|
-
|
|
5
|
-
Adding user-facing text to components or modifying existing strings.
|
|
6
|
-
|
|
7
|
-
## Pattern
|
|
8
|
-
|
|
9
|
-
**Library:** `i18n-react` — imported as `T`.
|
|
10
|
-
|
|
11
|
-
**Setup (in `registration-lite.js`):**
|
|
12
|
-
|
|
13
|
-
```javascript
|
|
14
|
-
import T from 'i18n-react';
|
|
15
|
-
|
|
16
|
-
// Loaded once at component init, falls back to English
|
|
17
|
-
try {
|
|
18
|
-
T.setTexts(require(`../i18n/${language}.json`));
|
|
19
|
-
} catch {
|
|
20
|
-
T.setTexts(require(`../i18n/en.json`));
|
|
21
|
-
}
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
**Usage in components:**
|
|
25
|
-
|
|
26
|
-
```javascript
|
|
27
|
-
import T from 'i18n-react';
|
|
28
|
-
|
|
29
|
-
// Simple key
|
|
30
|
-
T.translate("bar_button.next") // → "Next"
|
|
31
|
-
|
|
32
|
-
// With interpolation
|
|
33
|
-
T.translate("purchase_complete_step.footer_assistance_text", { supportEmail: "help@example.com" })
|
|
34
|
-
// Template: "For further assistance, please email <a href=\"mailto:{supportEmail}\">{supportEmail}</a>"
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
## Translation File Structure
|
|
38
|
-
|
|
39
|
-
`src/i18n/en.json` — flat namespaced by UI section:
|
|
40
|
-
|
|
41
|
-
```json
|
|
42
|
-
{
|
|
43
|
-
"purchase_complete_step": { "title": "...", "initial_order_complete_button": "..." },
|
|
44
|
-
"ticket_type": { "ticket_quantity_tooltip": "..." },
|
|
45
|
-
"promo_code": { "promo_code_tooltip": "..." },
|
|
46
|
-
"bar_button": { "back": "Back", "next": "Next", "get_ticket": "Get Ticket", "pay_now": "Pay Now", "loading": "Loading" }
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Key Points
|
|
51
|
-
|
|
52
|
-
- Namespace keys by component/step: `"section_name.key_name"`
|
|
53
|
-
- Interpolation uses `{variableName}` syntax (not `${}` or `%s`)
|
|
54
|
-
- HTML is allowed in translation values (rendered as-is)
|
|
55
|
-
- Only English (`en.json`) exists — add new languages as `{lang}.json` in `src/i18n/`
|
|
56
|
-
- Components using text must `import T from 'i18n-react'`
|
|
57
|
-
|
|
58
|
-
## Common Mistakes
|
|
59
|
-
|
|
60
|
-
- Hardcoding user-facing strings instead of using `T.translate()`
|
|
61
|
-
- Adding keys without updating `en.json` (renders the key path as literal text)
|
|
62
|
-
- Using wrong namespace (check `en.json` for existing sections before creating new ones)
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# Payment Provider Integration
|
|
2
|
-
|
|
3
|
-
## When to Apply
|
|
4
|
-
|
|
5
|
-
Adding or modifying payment providers (Stripe, LawPay, or new providers).
|
|
6
|
-
|
|
7
|
-
## Pattern
|
|
8
|
-
|
|
9
|
-
**Factory Pattern:** `PaymentProviderFactory.build(provider, params)` returns provider instance.
|
|
10
|
-
|
|
11
|
-
**Provider Structure:**
|
|
12
|
-
|
|
13
|
-
```javascript
|
|
14
|
-
export class NewProvider {
|
|
15
|
-
constructor({ reservation, summitId, userProfile, access_token, apiBaseUrl, dispatch }) {
|
|
16
|
-
// Store params as instance properties
|
|
17
|
-
this.reservation = reservation;
|
|
18
|
-
this.summitId = summitId;
|
|
19
|
-
// ... etc
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
payTicket = ({ /* provider-specific params */, onError = () => {} }) => async (dispatch) => {
|
|
23
|
-
// 1. Custom error handler for HTTP status codes
|
|
24
|
-
const errorHandler = (err, res) => (dispatch, state) => {
|
|
25
|
-
switch (err.status) {
|
|
26
|
-
case 404: onError({type: ERROR_TYPE_VALIDATION, msg: res.body.message, exception: null}); break;
|
|
27
|
-
case 500: onError({type: ERROR_TYPE_ERROR, msg: res.body.message, exception: null}); break;
|
|
28
|
-
default: authErrorHandler(err, res)(dispatch, state); break;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// 2. Free/prepaid orders skip payment gateway
|
|
33
|
-
if (isFreeOrder(this.reservation) || isPrePaidOrder(this.reservation)) {
|
|
34
|
-
return putRequest(/* ... checkout endpoint ... */)(params)(this.dispatch);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// 3. Payment gateway flow (provider-specific)
|
|
38
|
-
// Call gateway API, handle result, then checkout
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
**Factory Registration:**
|
|
44
|
-
|
|
45
|
-
```javascript
|
|
46
|
-
// src/utils/payment-providers/payment-provider-factory.js
|
|
47
|
-
import { NEW_PROVIDER } from '../constants';
|
|
48
|
-
import { NewProvider } from './new-provider';
|
|
49
|
-
|
|
50
|
-
case NEW_PROVIDER: {
|
|
51
|
-
currentProvider = new NewProvider({...params});
|
|
52
|
-
break;
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
## Key Points
|
|
57
|
-
|
|
58
|
-
- Free/prepaid orders always skip gateway, go straight to `/checkout` endpoint
|
|
59
|
-
- Error types: `ERROR_TYPE_VALIDATION` (404), `ERROR_TYPE_ERROR` (500), `ERROR_TYPE_PAYMENT` (gateway errors)
|
|
60
|
-
- Always dispatch `startWidgetLoading()` before async ops, `stopWidgetLoading()` after
|
|
61
|
-
- Success flow: checkout API → `CLEAR_RESERVATION` → `changeStep(STEP_COMPLETE)`
|
|
62
|
-
- Failure flow: `removeReservedTicket()` → `changeStep(STEP_PERSONAL_INFO)`
|
|
63
|
-
- Checkout endpoint requires billing address fields from `userProfile`
|
|
64
|
-
|
|
65
|
-
## Common Mistakes
|
|
66
|
-
|
|
67
|
-
- Forgetting to handle free/prepaid orders (they don't have `payment_gateway_client_token`)
|
|
68
|
-
- Not calling `removeReservedTicket()` on payment failure (leaves stale reservation)
|
|
69
|
-
- Missing `stopWidgetLoading()` in catch blocks (spinner never stops)
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# Project: Summit Registration Lite
|
|
2
|
-
|
|
3
|
-
**Last Updated:** 2026-03-05
|
|
4
|
-
|
|
5
|
-
## Overview
|
|
6
|
-
|
|
7
|
-
React widget for summit registration supporting authentication, ticket selection, payment processing, and order completion. Published as NPM package, designed to be embedded in parent applications.
|
|
8
|
-
|
|
9
|
-
## Technology Stack
|
|
10
|
-
|
|
11
|
-
- **Language:** JavaScript (ES6+)
|
|
12
|
-
- **Framework:** React 16+ with Redux for state management
|
|
13
|
-
- **Build Tool:** Webpack 5 (dev/prod configs)
|
|
14
|
-
- **Testing:** Jest with @testing-library/react
|
|
15
|
-
- **Package Manager:** Yarn
|
|
16
|
-
- **Key Libraries:**
|
|
17
|
-
- openstack-uicore-foundation (HTTP utils, actions)
|
|
18
|
-
- redux-persist (state persistence)
|
|
19
|
-
- Stripe.js and custom payment providers
|
|
20
|
-
- Material-UI (@mui/material)
|
|
21
|
-
|
|
22
|
-
## Directory Structure
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
summit-registration-lite/
|
|
26
|
-
├── src/
|
|
27
|
-
│ ├── components/ # React components (login, payment, tickets, etc.)
|
|
28
|
-
│ ├── utils/
|
|
29
|
-
│ │ ├── payment-providers/ # Stripe, LawPay provider implementations
|
|
30
|
-
│ │ ├── hooks/ # Custom React hooks
|
|
31
|
-
│ │ └── constants.js # Step constants, error types
|
|
32
|
-
│ ├── helpers/ # Formatting utilities
|
|
33
|
-
│ ├── actions.js # Redux action creators (API calls)
|
|
34
|
-
│ ├── reducer.js # Redux reducer
|
|
35
|
-
│ ├── store.js # Redux store configuration
|
|
36
|
-
│ └── summit-registration-lite.js # Main widget export
|
|
37
|
-
├── webpack.common.js
|
|
38
|
-
├── webpack.dev.js
|
|
39
|
-
├── webpack.prod.js
|
|
40
|
-
└── babel.config.js
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
## Key Files
|
|
44
|
-
|
|
45
|
-
- **Entry:** `src/summit-registration-lite.js` - Provider wrapper, exports widget and standalone login components
|
|
46
|
-
- **Main Component:** `src/components/registration-lite.js` - Multi-step registration flow
|
|
47
|
-
- **Actions:** `src/actions.js` - API integration (ticket types, reservations, payments)
|
|
48
|
-
- **Payment:** `src/utils/payment-providers/` - Factory pattern for payment provider abstraction
|
|
49
|
-
- **Config:** `webpack.common.js` - Shared webpack configuration
|
|
50
|
-
|
|
51
|
-
## Development Commands
|
|
52
|
-
|
|
53
|
-
| Task | Command |
|
|
54
|
-
|------|---------|
|
|
55
|
-
| Install | `yarn` |
|
|
56
|
-
| Dev Server | `yarn serve` |
|
|
57
|
-
| Build Dev | `yarn build-dev` |
|
|
58
|
-
| Build Prod | `yarn build` |
|
|
59
|
-
| Test (watch) | `yarn test` |
|
|
60
|
-
| Clean | `yarn clean` |
|
|
61
|
-
| Publish | `yarn publish-package` |
|
|
62
|
-
|
|
63
|
-
## Architecture Notes
|
|
64
|
-
|
|
65
|
-
- **State:** Redux with redux-persist for cross-session state
|
|
66
|
-
- **API Base:** Configurable via props (`apiBaseUrl`), uses openstack-uicore-foundation HTTP utils
|
|
67
|
-
- **Payment Providers:** Factory pattern (`PaymentProviderFactory`) supports Stripe and LawPay
|
|
68
|
-
- **Steps:** Multi-step flow defined in `utils/constants.js` (login, personal info, payment, complete)
|
|
69
|
-
- **Auth:** Supports native auth, OTP (passwordless), and configurable SSO providers via `loginOptions` prop
|
|
70
|
-
- **Integration:** Widget receives callbacks (`authUser`, `goToExtraQuestions`, `onPurchaseComplete`, etc.) for parent app control
|
|
71
|
-
|
|
72
|
-
## Testing
|
|
73
|
-
|
|
74
|
-
- Jest configuration in `package.json`
|
|
75
|
-
- Component tests in `__tests__` directories alongside components
|
|
76
|
-
- Mock files for static assets (`src/__mocks__/fileMock.js`)
|
|
77
|
-
|
|
78
|
-
## Debug Mode
|
|
79
|
-
|
|
80
|
-
URL hash override for testing time-based logic: `#now=2020-06-03,10:59:50` (summit timezone)
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# Redux API Action Patterns
|
|
2
|
-
|
|
3
|
-
## When to Apply
|
|
4
|
-
|
|
5
|
-
Creating new API actions or modifying existing ones in `src/actions.js`.
|
|
6
|
-
|
|
7
|
-
## Pattern
|
|
8
|
-
|
|
9
|
-
**Action Structure (openstack-uicore-foundation):**
|
|
10
|
-
|
|
11
|
-
```javascript
|
|
12
|
-
export const GET_DATA = 'GET_DATA';
|
|
13
|
-
|
|
14
|
-
export const getData = (summitId) => async (dispatch, getState) => {
|
|
15
|
-
const { apiBaseUrl } = getState().registration;
|
|
16
|
-
|
|
17
|
-
const params = {
|
|
18
|
-
access_token: await accessTokenSelector()(dispatch, getState),
|
|
19
|
-
expand: 'relations,nested.relations'
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
return getRequest(
|
|
23
|
-
null,
|
|
24
|
-
createAction(GET_DATA),
|
|
25
|
-
`${apiBaseUrl}/api/v1/summits/${summitId}/resource`,
|
|
26
|
-
customErrorHandler // optional
|
|
27
|
-
)(params)(dispatch);
|
|
28
|
-
};
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
**Error Handling:**
|
|
32
|
-
|
|
33
|
-
```javascript
|
|
34
|
-
const customErrorHandler = (err, res) => (dispatch, state) => {
|
|
35
|
-
if (err.timeout) return err;
|
|
36
|
-
if (res?.statusCode === 404) return err; // Handle specific codes
|
|
37
|
-
if (res?.statusCode === 500) return err;
|
|
38
|
-
return authErrorHandler(err, res)(dispatch, state); // Default auth handling
|
|
39
|
-
};
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
**Reducer Convention:**
|
|
43
|
-
|
|
44
|
-
```javascript
|
|
45
|
-
case GET_DATA: {
|
|
46
|
-
return {...state, data: payload.response};
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Key Points
|
|
51
|
-
|
|
52
|
-
- Actions exported as constants (`export const ACTION_NAME`) AND action creators (`export const actionName`)
|
|
53
|
-
- Use `createAction()` from openstack-uicore-foundation for action creators
|
|
54
|
-
- HTTP methods: `getRequest`, `postRequest`, `putRequest`, `deleteRequest`
|
|
55
|
-
- Always pass `params` object with `access_token` (use `accessTokenSelector()` or widget prop)
|
|
56
|
-
- Use `expand` param for nested relations (dot notation: `tickets.owner.extra_questions`)
|
|
57
|
-
- Custom error handlers must return `authErrorHandler` as default case
|
|
58
|
-
- Thunk signature: `(dispatch, getState) => { ... }`
|
|
59
|
-
|
|
60
|
-
## Common Mistakes
|
|
61
|
-
|
|
62
|
-
- Forgetting to await `accessTokenSelector()` (results in Promise instead of token)
|
|
63
|
-
- Not handling 404/500 in custom error handlers (triggers unwanted auth errors)
|
|
64
|
-
- Hardcoding API base URL (always use `state.registration.apiBaseUrl` or prop)
|
|
65
|
-
- Missing `expand` params (causes N+1 queries or missing data in components)
|