@akinon/next 2.0.0-beta.16 → 2.0.0-beta.17

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @akinon/next
2
2
 
3
+ ## 2.0.0-beta.17
4
+
5
+ ### Minor Changes
6
+
7
+ - 8218dafa: ZERO-4160: Refactor oauth-login middleware to use fetch for login and callback responses
8
+ - 2a8ddcf6: ZERO-4111: Add total balance field to store credit localization and update related components
9
+ - 8a7fd0f4: ZERO-4065: Add '[segment]' to skipSegments in route generation
10
+ - 36143125: ZERO-3987: Add barcode scanner functionality with modal and button
11
+ - f7e0f646: ZERO-4032: Add bfcache-headers middleware, integrate it into the default chain, and introduce a new environment variable for control.
12
+ - 94a86fcc: ZERO-4065: Expand skipSegments array to include additional segments for route generation
13
+ - bcaad120: ZERO-4158: Add logging for OAuth login and callback redirects
14
+
3
15
  ## 2.0.0-beta.16
4
16
 
5
17
  ### Minor Changes
@@ -0,0 +1,59 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import Settings from 'settings';
3
+
4
+ export async function GET(request: NextRequest) {
5
+ try {
6
+ const { searchParams } = new URL(request.url);
7
+ const barcode = searchParams.get('search_text');
8
+
9
+ if (!barcode) {
10
+ return NextResponse.json(
11
+ { error: 'Missing search_text parameter (barcode)' },
12
+ { status: 400 }
13
+ );
14
+ }
15
+
16
+ if (Settings.commerceUrl === 'default') {
17
+ return NextResponse.json(
18
+ { error: 'Commerce URL is not configured' },
19
+ { status: 500 }
20
+ );
21
+ }
22
+
23
+ const queryParams = new URLSearchParams();
24
+ queryParams.append('search_text', barcode);
25
+
26
+ searchParams.forEach((value, key) => {
27
+ if (key !== 'search_text') {
28
+ queryParams.append(key, value);
29
+ }
30
+ });
31
+
32
+ const apiUrl = `${Settings.commerceUrl}/list/?${queryParams.toString()}`;
33
+
34
+ const headers: Record<string, string> = {
35
+ Accept: 'application/json',
36
+ 'Content-Type': 'application/json'
37
+ };
38
+
39
+ const response = await fetch(apiUrl, {
40
+ method: 'GET',
41
+ headers
42
+ });
43
+
44
+ if (!response.ok) {
45
+ return NextResponse.json(
46
+ { error: `API request failed with status: ${response.status}` },
47
+ { status: response.status }
48
+ );
49
+ }
50
+
51
+ const data = await response.json();
52
+ return NextResponse.json(data);
53
+ } catch (error) {
54
+ return NextResponse.json(
55
+ { error: (error as Error).message },
56
+ { status: 500 }
57
+ );
58
+ }
59
+ }
@@ -25,7 +25,17 @@ const generateRoutes = () => {
25
25
  const routes = [];
26
26
  const excludedDirs = ['api', 'pz-not-found'];
27
27
 
28
- const skipSegments = ['[pz]', '[commerce]', '[locale]', '[currency]'];
28
+ const skipSegments = [
29
+ '[pz]',
30
+ '[commerce]',
31
+ '[locale]',
32
+ '[currency]',
33
+ '[session]',
34
+ '[segment]',
35
+ '[url]',
36
+ '[theme]',
37
+ '[member_type]'
38
+ ];
29
39
  const skipCatchAllRoutes = ['[...prettyurl]', '[...not_found]'];
30
40
 
31
41
  const walkDirectory = (dir, basePath = '') => {
@@ -55,6 +55,7 @@ export enum Component {
55
55
  SavedCard = 'SavedCardOption',
56
56
  VirtualTryOnPlugin = 'VirtualTryOnPlugin',
57
57
  BasketVirtualTryOn = 'BasketVirtualTryOn',
58
+ BarcodeScannerPlugin = 'BarcodeScannerPlugin',
58
59
  IyzicoSavedCard = 'IyzicoSavedCardOption',
59
60
  Hepsipay = 'Hepsipay',
60
61
  FlowPayment = 'FlowPayment',
@@ -117,7 +118,7 @@ const PluginComponents = new Map([
117
118
  [Plugin.FlowPayment, [Component.FlowPayment]],
118
119
  [
119
120
  Plugin.VirtualTryOn,
120
- [Component.VirtualTryOnPlugin, Component.BasketVirtualTryOn]
121
+ [Component.VirtualTryOnPlugin, Component.BasketVirtualTryOn, Component.BarcodeScannerPlugin]
121
122
  ],
122
123
  [Plugin.Hepsipay, [Component.Hepsipay]],
123
124
  [Plugin.MasterpassRest, [Component.MasterpassRest]],
@@ -7,6 +7,7 @@ import {
7
7
  AccountOrderCancellation,
8
8
  AccountOrderCancellationReason,
9
9
  ContactFormType,
10
+ LoyaltyBalanceItem,
10
11
  Order,
11
12
  Quotations
12
13
  } from '../../types';
@@ -220,7 +221,10 @@ const accountApi = api.injectEndpoints({
220
221
  method: 'PATCH'
221
222
  })
222
223
  }),
223
- getLoyaltyBalance: builder.query<{ balance: number }, void>({
224
+ getLoyaltyBalance: builder.query<
225
+ { balance: number; balances?: LoyaltyBalanceItem[] },
226
+ void
227
+ >({
224
228
  query: () => buildClientRequestUrl(account.loyaltyBalance)
225
229
  }),
226
230
  getLoyaltyTransactions: builder.query<LoyaltyTransactions, void>({
@@ -10,6 +10,7 @@ import {
10
10
  } from '../../redux/reducers/checkout';
11
11
  import {
12
12
  CheckoutContext,
13
+ AccountUsage,
13
14
  ExtraField,
14
15
  GuestLoginFormParams,
15
16
  Order,
@@ -776,16 +777,28 @@ export const checkoutApi = api.injectEndpoints({
776
777
  url: buildCheckoutRequestUrl(checkout.loyaltyMoneyUsage)
777
778
  })
778
779
  }),
779
- payWithLoyaltyBalance: build.mutation<any, string>({
780
- query: (amount) => ({
781
- url: buildCheckoutRequestUrl(checkout.loyaltyMoneyUsage, {
782
- useFormData: true
783
- }),
784
- method: 'POST',
785
- body: {
786
- loyalty_amount_to_use: amount
787
- }
788
- }),
780
+ payWithLoyaltyBalance: build.mutation<
781
+ any,
782
+ string | { account_usages: AccountUsage[] }
783
+ >({
784
+ query: (params) => {
785
+ const isAccountUsages =
786
+ typeof params === 'object' && 'account_usages' in params;
787
+
788
+ return {
789
+ url: buildCheckoutRequestUrl(checkout.loyaltyMoneyUsage, {
790
+ useFormData: true
791
+ }),
792
+ method: 'POST',
793
+ body: isAccountUsages
794
+ ? {
795
+ account_usages: JSON.stringify(params.account_usages)
796
+ }
797
+ : {
798
+ loyalty_amount_to_use: params
799
+ }
800
+ };
801
+ },
789
802
  async onQueryStarted(arg, { dispatch, queryFulfilled }) {
790
803
  dispatch(setPaymentStepBusy(true));
791
804
  dispatch(setPaymentOptions([]));
@@ -0,0 +1,18 @@
1
+ import { NextMiddleware } from 'next/server';
2
+
3
+ const withBfcacheHeaders = (middleware: NextMiddleware): NextMiddleware => {
4
+ return async (req, event) => {
5
+ const response = await middleware(req, event);
6
+
7
+ if (process.env.BF_CACHE === 'true' && response) {
8
+ response.headers.set(
9
+ 'Cache-Control',
10
+ 'private, no-cache, max-age=0, must-revalidate'
11
+ );
12
+ }
13
+
14
+ return response;
15
+ };
16
+ };
17
+
18
+ export default withBfcacheHeaders;
@@ -14,7 +14,8 @@ import {
14
14
  withUrlRedirection,
15
15
  withCompleteWallet,
16
16
  withWalletCompleteRedirection,
17
- withMasterpassRestCallback
17
+ withMasterpassRestCallback,
18
+ withBfcacheHeaders
18
19
  } from '.';
19
20
  import { urlLocaleMatcherRegex } from '../utils';
20
21
  import { getPzSegmentsConfig, encodePzValue, isLegacyMode } from '../utils/pz-segments';
@@ -256,10 +257,11 @@ const withPzDefault =
256
257
  withCompleteWallet(
257
258
  withWalletCompleteRedirection(
258
259
  withMasterpassRestCallback(
259
- async (
260
- req: PzNextRequest,
261
- event: NextFetchEvent
262
- ) => {
260
+ withBfcacheHeaders(
261
+ async (
262
+ req: PzNextRequest,
263
+ event: NextFetchEvent
264
+ ) => {
263
265
  let middlewareResult: NextResponse | void =
264
266
  NextResponse.next();
265
267
 
@@ -563,7 +565,7 @@ const withPzDefault =
563
565
  }
564
566
 
565
567
  return middlewareResult;
566
- }
568
+ })
567
569
  )
568
570
  )
569
571
  )
@@ -12,6 +12,7 @@ import withSavedCardRedirection from './saved-card-redirection';
12
12
  import withCompleteWallet from './complete-wallet';
13
13
  import withWalletCompleteRedirection from './wallet-complete-redirection';
14
14
  import withMasterpassRestCallback from './masterpass-rest-callback';
15
+ import withBfcacheHeaders from './bfcache-headers';
15
16
  import { NextRequest } from 'next/server';
16
17
 
17
18
  export {
@@ -28,7 +29,8 @@ export {
28
29
  withSavedCardRedirection,
29
30
  withCompleteWallet,
30
31
  withWalletCompleteRedirection,
31
- withMasterpassRestCallback
32
+ withMasterpassRestCallback,
33
+ withBfcacheHeaders
32
34
  };
33
35
 
34
36
  export interface PzNextRequest extends NextRequest {
@@ -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 withOauthLogin =
11
- (middleware: NextMiddleware) =>
12
- async (req: NextRequest, event: NextFetchEvent) => {
13
- const url = req.nextUrl.clone();
14
- const loginUrlMatcherRegex = new RegExp(/^\/(\w+)\/login\/?$/);
15
- const loginCallbackUrlMatcherRegex = new RegExp(
16
- /^\/(\w+)\/login\/callback\/?$/
17
- );
18
- const ip = req.headers.get('x-forwarded-for') ?? '';
19
-
20
- const headers = {
21
- 'x-forwarded-host':
22
- req.headers.get('x-forwarded-host') || req.headers.get('host') || '',
23
- 'x-currency': req.cookies.get('pz-currency')?.value ?? '',
24
- 'x-forwarded-for': ip
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
- if (loginUrlMatcherRegex.test(url.pathname)) {
28
- const provider = url.pathname.match(loginUrlMatcherRegex)[1];
29
- const response = NextResponse.rewrite(
30
- `${Settings.commerceUrl}/${provider}/login/`,
31
- {
32
- headers
33
- }
34
- );
103
+ const response = buildRedirectResponse(
104
+ commerceResponse,
105
+ location,
106
+ req.nextUrl.origin
107
+ );
35
108
 
36
- if (req.headers.get('referer')) {
37
- response.cookies.set(
38
- 'pz-oauth-callback-url',
39
- req.headers.get('referer')
40
- );
41
- }
109
+ response.headers.append(
110
+ 'set-cookie',
111
+ buildOAuthCallbackCookie(req.headers.get('referer') || '')
112
+ );
42
113
 
43
- return response;
44
- }
114
+ return { response, redirected: true };
115
+ }
45
116
 
46
- if (loginCallbackUrlMatcherRegex.test(url.pathname)) {
47
- const provider = url.pathname.match(loginCallbackUrlMatcherRegex)[1];
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
- return NextResponse.rewrite(
50
- `${Settings.commerceUrl}/${provider}/login/callback/${url.search}`,
51
- {
52
- headers
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
- if (!url.pathname.startsWith('/baskets/basket')) {
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
- const currentSessionId = req.cookies.get('osessionid');
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
- if (
64
- req.cookies.get('messages')?.value.includes('Successfully signed in') ||
65
- (currentSessionId && req.cookies.get('messages'))
66
- ) {
67
- let redirectUrlWithLocale = `${url.origin}${getUrlPathWithLocale(
68
- '/auth/oauth-login',
69
- req.cookies.get('pz-locale')?.value
70
- )}`;
71
- let callbackUrl = req.cookies.get('pz-oauth-callback-url')?.value ?? '';
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
- if (callbackUrl.length) {
74
- redirectUrlWithLocale += `?next=${encodeURIComponent(callbackUrl)}`;
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
- const response = NextResponse.redirect(redirectUrlWithLocale);
78
- response.cookies.delete('messages');
79
- response.cookies.delete('pz-oauth-callback-url');
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.16",
4
+ "version": "2.0.0-beta.17",
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.16",
38
+ "@akinon/eslint-plugin-projectzero": "2.0.0-beta.17",
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;