@akinon/next 2.0.0-beta.16 → 2.0.0-beta.18
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/CHANGELOG.md +22 -0
- package/api/barcode-search.ts +59 -0
- package/bin/pz-generate-routes.js +11 -1
- package/components/client-root.tsx +12 -2
- package/components/index.ts +1 -0
- package/components/logger-popup.tsx +213 -0
- package/components/plugin-module.tsx +2 -1
- package/data/client/account.ts +5 -1
- package/data/client/basket.ts +39 -0
- package/data/client/checkout.ts +207 -26
- package/hooks/index.ts +2 -0
- package/hooks/use-logger-context.tsx +114 -0
- package/hooks/use-logger.ts +92 -0
- package/middlewares/bfcache-headers.ts +18 -0
- package/middlewares/default.ts +8 -6
- package/middlewares/index.ts +3 -1
- package/middlewares/oauth-login.ts +200 -57
- package/package.json +2 -2
- package/redux/middlewares/checkout.ts +9 -0
- package/redux/reducers/checkout.ts +6 -0
- package/types/commerce/checkout.ts +15 -0
|
@@ -6,78 +6,221 @@ import {
|
|
|
6
6
|
} from 'next/server';
|
|
7
7
|
import Settings from 'settings';
|
|
8
8
|
import { getUrlPathWithLocale } from '../utils/localization';
|
|
9
|
+
import logger from '../utils/log';
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
11
|
+
const LOGIN_URL_REGEX = /^\/(\w+)\/login\/?$/;
|
|
12
|
+
const CALLBACK_URL_REGEX = /^\/(\w+)\/login\/callback\/?$/;
|
|
13
|
+
|
|
14
|
+
function buildCommerceHeaders(req: NextRequest): Record<string, string> {
|
|
15
|
+
return {
|
|
16
|
+
'x-forwarded-host':
|
|
17
|
+
req.headers.get('x-forwarded-host') || req.headers.get('host') || '',
|
|
18
|
+
'x-forwarded-for': req.headers.get('x-forwarded-for') ?? '',
|
|
19
|
+
'x-forwarded-proto': req.headers.get('x-forwarded-proto') || 'https',
|
|
20
|
+
'x-currency': req.cookies.get('pz-currency')?.value ?? '',
|
|
21
|
+
cookie: req.headers.get('cookie') ?? ''
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function getRequestBody(
|
|
26
|
+
req: NextRequest
|
|
27
|
+
): Promise<{ content: string; contentType: string } | undefined> {
|
|
28
|
+
if (req.method !== 'POST') return undefined;
|
|
29
|
+
|
|
30
|
+
const content = await req.text();
|
|
31
|
+
if (!content.length) return undefined;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
content,
|
|
35
|
+
contentType:
|
|
36
|
+
req.headers.get('content-type') ?? 'application/x-www-form-urlencoded'
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fetchCommerce(
|
|
41
|
+
url: string,
|
|
42
|
+
req: NextRequest,
|
|
43
|
+
body?: { content: string; contentType: string }
|
|
44
|
+
): Promise<Response> {
|
|
45
|
+
const headers = buildCommerceHeaders(req);
|
|
46
|
+
|
|
47
|
+
if (body) {
|
|
48
|
+
headers['content-type'] = body.contentType;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return fetch(url, {
|
|
52
|
+
method: body ? 'POST' : 'GET',
|
|
53
|
+
headers,
|
|
54
|
+
body: body?.content,
|
|
55
|
+
redirect: 'manual'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function forwardCookies(from: Response, to: NextResponse): void {
|
|
60
|
+
from.headers.getSetCookie().forEach((cookie) => {
|
|
61
|
+
to.headers.append('set-cookie', cookie);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildRedirectResponse(
|
|
66
|
+
commerceResponse: Response,
|
|
67
|
+
location: string,
|
|
68
|
+
origin: string
|
|
69
|
+
): NextResponse {
|
|
70
|
+
const response = NextResponse.redirect(new URL(location, origin));
|
|
71
|
+
forwardCookies(commerceResponse, response);
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function commercePassthrough(commerceResponse: Response): NextResponse {
|
|
76
|
+
return new NextResponse(commerceResponse.body, {
|
|
77
|
+
status: commerceResponse.status,
|
|
78
|
+
headers: commerceResponse.headers
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildOAuthCallbackCookie(referer: string): string {
|
|
83
|
+
return `pz-oauth-callback-url=${encodeURIComponent(referer)}; Path=/`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function handleLogin(
|
|
87
|
+
req: NextRequest,
|
|
88
|
+
provider: string
|
|
89
|
+
): Promise<{ response: NextResponse; redirected: boolean }> {
|
|
90
|
+
const commerceResponse = await fetchCommerce(
|
|
91
|
+
`${Settings.commerceUrl}/${provider}/login/`,
|
|
92
|
+
req
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const location = commerceResponse.headers.get('location');
|
|
96
|
+
if (!location) {
|
|
97
|
+
return {
|
|
98
|
+
response: commercePassthrough(commerceResponse),
|
|
99
|
+
redirected: false
|
|
25
100
|
};
|
|
101
|
+
}
|
|
26
102
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
headers
|
|
33
|
-
}
|
|
34
|
-
);
|
|
103
|
+
const response = buildRedirectResponse(
|
|
104
|
+
commerceResponse,
|
|
105
|
+
location,
|
|
106
|
+
req.nextUrl.origin
|
|
107
|
+
);
|
|
35
108
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
}
|
|
109
|
+
response.headers.append(
|
|
110
|
+
'set-cookie',
|
|
111
|
+
buildOAuthCallbackCookie(req.headers.get('referer') || '')
|
|
112
|
+
);
|
|
42
113
|
|
|
43
|
-
|
|
44
|
-
|
|
114
|
+
return { response, redirected: true };
|
|
115
|
+
}
|
|
45
116
|
|
|
46
|
-
|
|
47
|
-
|
|
117
|
+
async function handleCallback(
|
|
118
|
+
req: NextRequest,
|
|
119
|
+
provider: string,
|
|
120
|
+
search: string
|
|
121
|
+
): Promise<{ response: NextResponse; redirected: boolean }> {
|
|
122
|
+
const body = await getRequestBody(req);
|
|
123
|
+
const commerceResponse = await fetchCommerce(
|
|
124
|
+
`${Settings.commerceUrl}/${provider}/login/callback/${search}`,
|
|
125
|
+
req,
|
|
126
|
+
body
|
|
127
|
+
);
|
|
48
128
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
129
|
+
const location = commerceResponse.headers.get('location');
|
|
130
|
+
if (!location) {
|
|
131
|
+
return {
|
|
132
|
+
response: commercePassthrough(commerceResponse),
|
|
133
|
+
redirected: false
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
response: buildRedirectResponse(
|
|
139
|
+
commerceResponse,
|
|
140
|
+
location,
|
|
141
|
+
req.nextUrl.origin
|
|
142
|
+
),
|
|
143
|
+
redirected: true
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function handleBasketRedirect(req: NextRequest): NextResponse | null {
|
|
148
|
+
const hasSession = req.cookies.get('osessionid');
|
|
149
|
+
const messages = req.cookies.get('messages')?.value;
|
|
150
|
+
|
|
151
|
+
if (!messages) return null;
|
|
152
|
+
if (!messages.includes('Successfully signed in') && !hasSession) return null;
|
|
153
|
+
|
|
154
|
+
let redirectUrl = `${req.nextUrl.origin}${getUrlPathWithLocale(
|
|
155
|
+
'/auth/oauth-login',
|
|
156
|
+
req.cookies.get('pz-locale')?.value
|
|
157
|
+
)}`;
|
|
158
|
+
|
|
159
|
+
const callbackUrl = req.cookies.get('pz-oauth-callback-url')?.value ?? '';
|
|
160
|
+
if (callbackUrl.length) {
|
|
161
|
+
redirectUrl += `?next=${encodeURIComponent(callbackUrl)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const response = NextResponse.redirect(redirectUrl);
|
|
165
|
+
response.cookies.delete('messages');
|
|
166
|
+
response.cookies.delete('pz-oauth-callback-url');
|
|
167
|
+
return response;
|
|
168
|
+
}
|
|
56
169
|
|
|
57
|
-
|
|
170
|
+
const withOauthLogin =
|
|
171
|
+
(middleware: NextMiddleware) =>
|
|
172
|
+
async (req: NextRequest, event: NextFetchEvent) => {
|
|
173
|
+
const { pathname, search } = req.nextUrl;
|
|
174
|
+
|
|
175
|
+
if (!pathname.includes('/login') && !pathname.startsWith('/baskets/basket')) {
|
|
58
176
|
return middleware(req, event);
|
|
59
177
|
}
|
|
60
178
|
|
|
61
|
-
|
|
179
|
+
logger.info('OAuth login redirect', {
|
|
180
|
+
host: req.headers.get('host'),
|
|
181
|
+
'x-forwarded-host': req.headers.get('x-forwarded-host'),
|
|
182
|
+
'x-forwarded-for': req.headers.get('x-forwarded-for')
|
|
183
|
+
});
|
|
62
184
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
185
|
+
const loginMatch = LOGIN_URL_REGEX.exec(pathname);
|
|
186
|
+
if (loginMatch) {
|
|
187
|
+
try {
|
|
188
|
+
const { response, redirected } = await handleLogin(req, loginMatch[1]);
|
|
189
|
+
if (!redirected) {
|
|
190
|
+
logger.warn('OAuth login: no redirect from commerce', {
|
|
191
|
+
provider: loginMatch[1]
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return response;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.error('OAuth login fetch failed', { error });
|
|
197
|
+
return middleware(req, event);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
72
200
|
|
|
73
|
-
|
|
74
|
-
|
|
201
|
+
const callbackMatch = CALLBACK_URL_REGEX.exec(pathname);
|
|
202
|
+
if (callbackMatch) {
|
|
203
|
+
try {
|
|
204
|
+
const { response, redirected } = await handleCallback(
|
|
205
|
+
req,
|
|
206
|
+
callbackMatch[1],
|
|
207
|
+
search
|
|
208
|
+
);
|
|
209
|
+
if (!redirected) {
|
|
210
|
+
logger.warn('OAuth callback: no redirect from commerce', {
|
|
211
|
+
provider: callbackMatch[1]
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return response;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger.error('OAuth callback fetch failed', { error });
|
|
217
|
+
return middleware(req, event);
|
|
75
218
|
}
|
|
219
|
+
}
|
|
76
220
|
|
|
77
|
-
|
|
78
|
-
response
|
|
79
|
-
response
|
|
80
|
-
return response;
|
|
221
|
+
if (pathname.startsWith('/baskets/basket')) {
|
|
222
|
+
const response = handleBasketRedirect(req);
|
|
223
|
+
if (response) return response;
|
|
81
224
|
}
|
|
82
225
|
|
|
83
226
|
return middleware(req, event);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/next",
|
|
3
3
|
"description": "Core package for Project Zero Next",
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.18",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"set-cookie-parser": "2.6.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@akinon/eslint-plugin-projectzero": "2.0.0-beta.
|
|
38
|
+
"@akinon/eslint-plugin-projectzero": "2.0.0-beta.18",
|
|
39
39
|
"@babel/core": "7.26.10",
|
|
40
40
|
"@babel/preset-env": "7.26.9",
|
|
41
41
|
"@babel/preset-typescript": "7.27.0",
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
setHasGiftBox,
|
|
15
15
|
setInstallmentOptions,
|
|
16
16
|
setLoyaltyBalance,
|
|
17
|
+
setLoyaltyBalances,
|
|
17
18
|
setPaymentChoices,
|
|
18
19
|
setPaymentOptions,
|
|
19
20
|
setRetailStores,
|
|
@@ -224,6 +225,14 @@ export const contextListMiddleware: Middleware = ({
|
|
|
224
225
|
dispatch(setLoyaltyBalance(context.page_context.balance));
|
|
225
226
|
}
|
|
226
227
|
|
|
228
|
+
if (context.page_context.balances) {
|
|
229
|
+
dispatch(setLoyaltyBalances(context.page_context.balances));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (context.page_context.accounts) {
|
|
233
|
+
dispatch(setLoyaltyBalances(context.page_context.accounts));
|
|
234
|
+
}
|
|
235
|
+
|
|
227
236
|
if (context.page_context.retail_stores) {
|
|
228
237
|
dispatch(setRetailStores(context.page_context.retail_stores));
|
|
229
238
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CreditCardType,
|
|
10
10
|
DeliveryOption,
|
|
11
11
|
InstallmentOption,
|
|
12
|
+
LoyaltyBalanceItem,
|
|
12
13
|
PaymentChoice,
|
|
13
14
|
PaymentOption,
|
|
14
15
|
CheckoutCreditPaymentOption,
|
|
@@ -49,6 +50,7 @@ export interface CheckoutState {
|
|
|
49
50
|
bankAccounts: BankAccount[];
|
|
50
51
|
selectedBankAccountPk: number;
|
|
51
52
|
loyaltyBalance?: string;
|
|
53
|
+
loyaltyBalances?: LoyaltyBalanceItem[];
|
|
52
54
|
retailStores: RetailStore[];
|
|
53
55
|
attributeBasedShippingOptions: AttributeBasedShippingOption[];
|
|
54
56
|
selectedShippingOptions: Record<string, number>;
|
|
@@ -188,6 +190,9 @@ const checkoutSlice = createSlice({
|
|
|
188
190
|
setLoyaltyBalance(state, { payload }) {
|
|
189
191
|
state.loyaltyBalance = payload;
|
|
190
192
|
},
|
|
193
|
+
setLoyaltyBalances(state, { payload }) {
|
|
194
|
+
state.loyaltyBalances = payload;
|
|
195
|
+
},
|
|
191
196
|
setRetailStores(state, { payload }) {
|
|
192
197
|
state.retailStores = payload;
|
|
193
198
|
},
|
|
@@ -234,6 +239,7 @@ export const {
|
|
|
234
239
|
setBankAccounts,
|
|
235
240
|
setSelectedBankAccountPk,
|
|
236
241
|
setLoyaltyBalance,
|
|
242
|
+
setLoyaltyBalances,
|
|
237
243
|
setRetailStores,
|
|
238
244
|
setAttributeBasedShippingOptions,
|
|
239
245
|
setSelectedShippingOptions,
|
|
@@ -68,6 +68,18 @@ export interface CheckoutPaymentOption {
|
|
|
68
68
|
viewProps?: any;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export interface LoyaltyBalanceItem {
|
|
72
|
+
label_id: number | null;
|
|
73
|
+
label: string | null;
|
|
74
|
+
balance: string;
|
|
75
|
+
currency?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface AccountUsage {
|
|
79
|
+
label_id: number | null;
|
|
80
|
+
amount: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
71
83
|
export interface GiftBox {
|
|
72
84
|
note: string;
|
|
73
85
|
gift_video: boolean;
|
|
@@ -91,6 +103,7 @@ export interface PreOrder {
|
|
|
91
103
|
notes?: string;
|
|
92
104
|
user_phone_number?: string;
|
|
93
105
|
loyalty_money?: string;
|
|
106
|
+
loyalty_account_usages?: AccountUsage[];
|
|
94
107
|
currency_type_label?: string;
|
|
95
108
|
is_guest?: boolean;
|
|
96
109
|
is_post_order?: boolean;
|
|
@@ -151,6 +164,8 @@ export interface CheckoutContext {
|
|
|
151
164
|
redirect_url?: string;
|
|
152
165
|
context_data?: any;
|
|
153
166
|
balance?: string;
|
|
167
|
+
balances?: LoyaltyBalanceItem[];
|
|
168
|
+
accounts?: LoyaltyBalanceItem[];
|
|
154
169
|
attribute_based_shipping_options?: AttributeBasedShippingOption[];
|
|
155
170
|
paymentData?: any;
|
|
156
171
|
paymentMethod?: string;
|