orionplace-react 0.1.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/LICENSE +21 -0
- package/README.md +242 -0
- package/bin/orionplace-react.mjs +101 -0
- package/package.json +56 -0
- package/runtime/marketplace.d.mts +22 -0
- package/runtime/marketplace.mjs +211 -0
- package/src/project-readme.mjs +341 -0
- package/src/setup.mjs +250 -0
- package/src/update.mjs +36 -0
- package/src/validation.mjs +27 -0
- package/templates/app/api/[...orionplace]/route.ts +14 -0
- package/templates/app/globals.css +10 -0
- package/templates/app/layout.tsx +25 -0
- package/templates/app/page.module.css +133 -0
- package/templates/app/page.tsx +80 -0
- package/templates/hooks/useOrionplaceAppLoad.ts +34 -0
- package/templates/interfaces/orionplace/apiBase/TMethod.ts +1 -0
- package/templates/interfaces/orionplace/appBase/IAppLoad.ts +15 -0
- package/templates/interfaces/orionplace/appBase/IAppLoadCommon.ts +54 -0
- package/templates/interfaces/orionplace/appBase/IApploadUserProfile.ts +13 -0
- package/templates/interfaces/orionplace/appBase/IForm.ts +21 -0
- package/templates/interfaces/orionplace/appBase/ISEO.ts +21 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonBanner.ts +11 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonContact.ts +8 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonCurrency.ts +15 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonEvents.ts +18 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLocale.ts +14 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonLogos.ts +6 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonPath.ts +22 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSEO.ts +13 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonSocial.ts +15 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonTariff.ts +7 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonUnit.ts +12 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVariables.ts +7 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonVisualSettings.ts +11 -0
- package/templates/interfaces/orionplace/appBase/appLoadCommon/IAppLoadCommonWalletValue.ts +3 -0
- package/templates/interfaces/orionplace/category/ICategory.ts +40 -0
- package/templates/interfaces/orionplace/domains/IDomain.ts +13 -0
- package/templates/interfaces/orionplace/index.ts +30 -0
- package/templates/interfaces/orionplace/products/IProduct.ts +235 -0
- package/templates/interfaces/orionplace/shared/IGeoCoordinates.ts +4 -0
- package/templates/interfaces/orionplace/shared/ITransformValue.ts +3 -0
- package/templates/interfaces/orionplace/state.ts +4 -0
- package/templates/interfaces/orionplace/subscribeOptions/ISubscribeOption.ts +9 -0
- package/templates/interfaces/orionplace/subscribeTariff/ISubscribeTariff.ts +46 -0
- package/templates/interfaces/orionplace/subscribes/ISubscribe.ts +28 -0
- package/templates/interfaces/orionplace/transport.ts +30 -0
- package/templates/lib/orionplace/appLoadClient.ts +33 -0
- package/templates/lib/orionplace/appLoadResult.ts +56 -0
- package/templates/lib/orionplace/appLoadServer.ts +72 -0
- package/templates/lib/orionplace/domain.ts +16 -0
- package/templates/providers/OrionplaceProvider.tsx +39 -0
- package/templates/proxy.ts +17 -0
- package/templates/stores/orionplace.ts +119 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ITransformValue } from '../shared/ITransformValue';
|
|
2
|
+
|
|
3
|
+
export interface IDomain {
|
|
4
|
+
id: number;
|
|
5
|
+
domain_extras: IDomainExtras;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface IDomainExtras {
|
|
9
|
+
period_id: number;
|
|
10
|
+
option_cost_id: number;
|
|
11
|
+
amount: Record<string, ITransformValue>;
|
|
12
|
+
days: number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export * from './apiBase/TMethod';
|
|
2
|
+
export * from './appBase/IAppLoad';
|
|
3
|
+
export * from './appBase/IAppLoadCommon';
|
|
4
|
+
export * from './appBase/IApploadUserProfile';
|
|
5
|
+
export * from './appBase/IForm';
|
|
6
|
+
export * from './appBase/ISEO';
|
|
7
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonBanner';
|
|
8
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonContact';
|
|
9
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonCurrency';
|
|
10
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonEvents';
|
|
11
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonLocale';
|
|
12
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonLogos';
|
|
13
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonPath';
|
|
14
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonSEO';
|
|
15
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonSocial';
|
|
16
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonTariff';
|
|
17
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonUnit';
|
|
18
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonVariables';
|
|
19
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonVisualSettings';
|
|
20
|
+
export * from './appBase/appLoadCommon/IAppLoadCommonWalletValue';
|
|
21
|
+
export * from './category/ICategory';
|
|
22
|
+
export * from './domains/IDomain';
|
|
23
|
+
export * from './products/IProduct';
|
|
24
|
+
export * from './shared/IGeoCoordinates';
|
|
25
|
+
export * from './shared/ITransformValue';
|
|
26
|
+
export * from './subscribeOptions/ISubscribeOption';
|
|
27
|
+
export * from './subscribeTariff/ISubscribeTariff';
|
|
28
|
+
export * from './subscribes/ISubscribe';
|
|
29
|
+
export * from './transport';
|
|
30
|
+
export * from './state';
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import type { ITransformValue } from '../shared/ITransformValue';
|
|
2
|
+
|
|
3
|
+
export interface IProduct {
|
|
4
|
+
id: number;
|
|
5
|
+
product_group_id: number;
|
|
6
|
+
whishlist_id?: number | null;
|
|
7
|
+
main_category?: number | null;
|
|
8
|
+
subscription_id?: number | null;
|
|
9
|
+
is_digital?: boolean;
|
|
10
|
+
locale: Record<string, Partial<IProductLocale>>;
|
|
11
|
+
discount: number;
|
|
12
|
+
cart_counts?: IProductCartCounts | null;
|
|
13
|
+
base_price_dict: Record<string, ITransformValue>;
|
|
14
|
+
product_price_dict: Record<string, ITransformValue>;
|
|
15
|
+
images?: IProductImages | null;
|
|
16
|
+
weight_dict: Record<string, ITransformValue>;
|
|
17
|
+
length_dict: IProductDimensions;
|
|
18
|
+
variants: IProductVariants;
|
|
19
|
+
flags: IProductFlags;
|
|
20
|
+
seller: IProductSeller;
|
|
21
|
+
status: IProductStatus;
|
|
22
|
+
c_real_left: number;
|
|
23
|
+
feedback_stat: IProductFeedbackStat;
|
|
24
|
+
related?: IProductRelated | null;
|
|
25
|
+
secondary_variants?: IProductSecondaryVariants;
|
|
26
|
+
feedback_allowed?: boolean;
|
|
27
|
+
attributes?: IProductAttribute[] | Record<string, IProductAttribute>;
|
|
28
|
+
videos?: IProductVideo[];
|
|
29
|
+
manufacture?: IProductManufacture | null;
|
|
30
|
+
refunds?: IProductRefunds | null;
|
|
31
|
+
feedbacks?: IProductFeedbacks | null;
|
|
32
|
+
promo?: IProductPromo | null;
|
|
33
|
+
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface IProductAttribute {
|
|
37
|
+
id: number;
|
|
38
|
+
names: IProductAttributeName[];
|
|
39
|
+
locales: Record<string, string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface IProductAttributeName {
|
|
43
|
+
id: number;
|
|
44
|
+
locale: Record<string, Record<string, string>>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface IProductVideo {
|
|
48
|
+
id: number;
|
|
49
|
+
product_video: string;
|
|
50
|
+
product: number;
|
|
51
|
+
preview: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface IProductManufacture {
|
|
55
|
+
id: number;
|
|
56
|
+
name: string;
|
|
57
|
+
image: string;
|
|
58
|
+
small_image: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface IProductRefunds {
|
|
62
|
+
customer_max_refund_days: number;
|
|
63
|
+
max_refund_days_with_defect: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface IProductPromo {
|
|
67
|
+
id: number;
|
|
68
|
+
locale: Record<string, string | { promo_name?: string; name?: string }>;
|
|
69
|
+
promo_value: number | Record<string, ITransformValue>;
|
|
70
|
+
promo_type: 'FREE_SHIPPING' | 'FIXED' | 'PERCENTAGE';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface IProductFeedbacks {
|
|
74
|
+
feedbacks?: {
|
|
75
|
+
data: IProductFeedback[];
|
|
76
|
+
rpag?: IProductPagination;
|
|
77
|
+
};
|
|
78
|
+
stats?: Record<string, number>;
|
|
79
|
+
c_feedback_images?: number;
|
|
80
|
+
image_row?: { data: unknown[]; rpag?: IProductPagination };
|
|
81
|
+
our_feedback_id?: number | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface IProductFeedback {
|
|
85
|
+
id: number;
|
|
86
|
+
rating?: number;
|
|
87
|
+
text?: string;
|
|
88
|
+
created_at?: string;
|
|
89
|
+
date_created?: string;
|
|
90
|
+
user?: { id?: number; name?: string; avatar?: string };
|
|
91
|
+
customer?: { id?: number; name?: string; avatar?: string };
|
|
92
|
+
images?: Array<string | { image?: string; url?: string }>;
|
|
93
|
+
[key: string]: unknown;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface IProductPagination {
|
|
97
|
+
has_next?: boolean;
|
|
98
|
+
has_previous?: boolean;
|
|
99
|
+
page?: number;
|
|
100
|
+
pages?: number;
|
|
101
|
+
total_count?: number;
|
|
102
|
+
selected_count?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface IProductFeedbackStat {
|
|
106
|
+
c_feedbacks: number;
|
|
107
|
+
avg_raiting: number | string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface IProductStatus {
|
|
111
|
+
status: EProductStatus;
|
|
112
|
+
status_display: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const EProductStatus = {
|
|
116
|
+
PUBLISHED: 2,
|
|
117
|
+
PUBLISHED_AS_OPTION: 1,
|
|
118
|
+
UNPUBLISHED: 0,
|
|
119
|
+
TEMPORARY_STOPPED: -1,
|
|
120
|
+
LOW_SALE_STOCK: -2,
|
|
121
|
+
LOW_STOCK: -3,
|
|
122
|
+
INCOMPLETE: -4,
|
|
123
|
+
ON_MODERATION: -5,
|
|
124
|
+
} as const;
|
|
125
|
+
export type EProductStatus =
|
|
126
|
+
(typeof EProductStatus)[keyof typeof EProductStatus];
|
|
127
|
+
|
|
128
|
+
export interface IProductSeller {
|
|
129
|
+
id: number;
|
|
130
|
+
name: string;
|
|
131
|
+
raiting: number | string;
|
|
132
|
+
avatar?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface IProductFlags {
|
|
136
|
+
flag_is_new?: boolean;
|
|
137
|
+
flag_is_soon_be_out?: boolean;
|
|
138
|
+
flag_is_popular?: boolean;
|
|
139
|
+
flag_is_good_deal?: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface IProductVariants {
|
|
143
|
+
main_variant?: IProductVariant;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface IProductVariant {
|
|
147
|
+
id: number;
|
|
148
|
+
value: IProductVariantValue[];
|
|
149
|
+
locale: Record<string, string>;
|
|
150
|
+
is_color: boolean;
|
|
151
|
+
second_exists: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface IProductVariantValue {
|
|
155
|
+
id: number;
|
|
156
|
+
color: string;
|
|
157
|
+
locale: Record<string, string>;
|
|
158
|
+
c_published: number;
|
|
159
|
+
reload_product?: number | null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface IProductDimensions {
|
|
163
|
+
length: Record<string, ITransformValue>;
|
|
164
|
+
width: Record<string, ITransformValue>;
|
|
165
|
+
height: Record<string, ITransformValue>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface IProductImages {
|
|
169
|
+
small: string[];
|
|
170
|
+
full: string[];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface IProductCartCounts {
|
|
174
|
+
modal_quantity?: number;
|
|
175
|
+
cart_id?: number;
|
|
176
|
+
cart_item_price_no_discount?: Record<string, ITransformValue>;
|
|
177
|
+
cart_item_price_after_promo?: Record<string, ITransformValue>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface IProductRelated {
|
|
181
|
+
data: IProduct[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface IProductSecondaryVariants {
|
|
185
|
+
secondary: IProductSecondaryVariantInfo;
|
|
186
|
+
variants: IProductSecondaryVariant[];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface IProductSecondaryVariantInfo {
|
|
190
|
+
id: number;
|
|
191
|
+
is_color: boolean;
|
|
192
|
+
locale: Record<string, string>;
|
|
193
|
+
value: null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export interface IProductSecondaryVariant {
|
|
197
|
+
product: IProduct;
|
|
198
|
+
value: IProductSecondaryVariantValue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface IProductSecondaryVariantValue {
|
|
202
|
+
id: number;
|
|
203
|
+
locale: Record<string, string>;
|
|
204
|
+
color: string;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface IProductLocale {
|
|
208
|
+
id?: number;
|
|
209
|
+
product_name?: string;
|
|
210
|
+
product_title?: string;
|
|
211
|
+
product_slug?: string;
|
|
212
|
+
meta_description?: string;
|
|
213
|
+
meta_keywords?: string;
|
|
214
|
+
product_description?: string;
|
|
215
|
+
short_description?: string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type IProductsListNames =
|
|
219
|
+
| 'main'
|
|
220
|
+
| 'new'
|
|
221
|
+
| 'featured'
|
|
222
|
+
| 'seller'
|
|
223
|
+
| 'manufacturer'
|
|
224
|
+
| 'productAlerts'
|
|
225
|
+
| 'productAlertsRelated'
|
|
226
|
+
| 'cart'
|
|
227
|
+
| 'cartPreview'
|
|
228
|
+
| 'cartUndeliverable'
|
|
229
|
+
| 'orders'
|
|
230
|
+
| 'feedbacks';
|
|
231
|
+
|
|
232
|
+
export interface IProcessingProduct {
|
|
233
|
+
id: number;
|
|
234
|
+
listName: IProductsListNames;
|
|
235
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { JsonValue } from '../transport';
|
|
2
|
+
import type { ITransformValue } from '../shared/ITransformValue';
|
|
3
|
+
import type { ISubscribeOptionLocale } from '../subscribeOptions/ISubscribeOption';
|
|
4
|
+
import type { IDomainExtras } from '../domains/IDomain';
|
|
5
|
+
|
|
6
|
+
export interface ISubscribeTariff {
|
|
7
|
+
id: number;
|
|
8
|
+
locales: Record<string, ISubscribeTariffLocales>;
|
|
9
|
+
tariff_variables_json: Record<string, JsonValue>;
|
|
10
|
+
trial_days: number;
|
|
11
|
+
prices: ISubscribeTariffPrice[];
|
|
12
|
+
subscription: number;
|
|
13
|
+
tariff_image: ISubscribeTariffImages | null;
|
|
14
|
+
options: ISubscribeTariffOption[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ISubscribeTariffOption {
|
|
18
|
+
id: number;
|
|
19
|
+
locale: Record<string, ISubscribeOptionLocale>;
|
|
20
|
+
image: string | null;
|
|
21
|
+
eligiable_for_trial: boolean;
|
|
22
|
+
domain_extras: IDomainExtras[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ISubscribeTariffImages {
|
|
26
|
+
small: string[];
|
|
27
|
+
full: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ISubscribeTariffPrice {
|
|
31
|
+
period_id: number;
|
|
32
|
+
tariff_cost_id: number;
|
|
33
|
+
amount: Record<string, ITransformValue>;
|
|
34
|
+
days: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ISubscribeTariffLocales {
|
|
38
|
+
id?: number;
|
|
39
|
+
tariff_name?: string;
|
|
40
|
+
tariff_title?: string;
|
|
41
|
+
tariff_slug?: string;
|
|
42
|
+
tariff_meta_description?: string;
|
|
43
|
+
tariff_meta_keywords?: string;
|
|
44
|
+
tariff_description?: string;
|
|
45
|
+
tariff_short_description?: string;
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JsonValue } from '../transport';
|
|
2
|
+
export interface ISubscribe {
|
|
3
|
+
id: number;
|
|
4
|
+
locale: Record<string, ISubscribeLocale>;
|
|
5
|
+
is_new_subs_allowed: boolean;
|
|
6
|
+
only_admin_can_subscribe: boolean;
|
|
7
|
+
translations: Record<string, JsonValue>;
|
|
8
|
+
user_data: Record<string, JsonValue>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ISubscribeUserData {
|
|
12
|
+
headings: Record<string, ISubscribeUserDataValue>;
|
|
13
|
+
hosts?: ISubscribeUserDataValue;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ISubscribeUserDataValue {
|
|
17
|
+
tabs: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ISubscribeTranslations {
|
|
21
|
+
id: number;
|
|
22
|
+
trans: Record<string, string>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ISubscribeLocale {
|
|
26
|
+
subscription_name?: string;
|
|
27
|
+
subscription_description?: string;
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { IAppLoad } from './appBase/IAppLoad';
|
|
2
|
+
|
|
3
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
|
|
4
|
+
export interface AppLoadRequest {
|
|
5
|
+
detalize_mode: 'full' | 'light';
|
|
6
|
+
ignore_cache: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ApiFormError { message: string; code: string }
|
|
9
|
+
export interface AppLoadResponse {
|
|
10
|
+
form_errors: Record<string, ApiFormError[]> | null;
|
|
11
|
+
success_message: string | null;
|
|
12
|
+
page_data: IAppLoad | null;
|
|
13
|
+
locale?: string;
|
|
14
|
+
url_name?: string;
|
|
15
|
+
exc_stack?: JsonValue;
|
|
16
|
+
debug?: JsonValue;
|
|
17
|
+
user_groups?: JsonValue;
|
|
18
|
+
user_perms?: JsonValue;
|
|
19
|
+
spent?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface AppLoadResult {
|
|
22
|
+
response: AppLoadResponse & { page_data: IAppLoad };
|
|
23
|
+
httpStatus: number;
|
|
24
|
+
loadedAt: string;
|
|
25
|
+
}
|
|
26
|
+
export interface AppLoadFailure {
|
|
27
|
+
kind: 'http' | 'validation' | 'invalid-response' | 'network';
|
|
28
|
+
httpStatus: number | null;
|
|
29
|
+
fields: string[];
|
|
30
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AppLoadError, appLoadResult, inspectAppLoad } from './appLoadResult';
|
|
2
|
+
import type { AppLoadRequest, AppLoadResult } from '../../interfaces/orionplace/transport';
|
|
3
|
+
|
|
4
|
+
function withTimeout(signal: AbortSignal, milliseconds: number): AbortSignal {
|
|
5
|
+
if (typeof AbortSignal.any !== 'function') return signal;
|
|
6
|
+
return AbortSignal.any([signal, AbortSignal.timeout(milliseconds)]);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Browser transport for explicit initialize and refresh actions. The browser
|
|
11
|
+
* sends and stores session cookies itself through the same-origin gateway.
|
|
12
|
+
*/
|
|
13
|
+
export async function requestBrowserAppLoad(signal: AbortSignal): Promise<AppLoadResult> {
|
|
14
|
+
let response: Response;
|
|
15
|
+
try {
|
|
16
|
+
response = await fetch('/api/appload', {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({ detalize_mode: 'light', ignore_cache: true } satisfies AppLoadRequest),
|
|
20
|
+
credentials: 'same-origin',
|
|
21
|
+
cache: 'no-store',
|
|
22
|
+
signal: withTimeout(signal, 35_000),
|
|
23
|
+
});
|
|
24
|
+
} catch {
|
|
25
|
+
throw new AppLoadError('Could not reach the marketplace API.', { kind: 'network', httpStatus: null, fields: [] });
|
|
26
|
+
}
|
|
27
|
+
let body: unknown = null;
|
|
28
|
+
try { body = await response.json(); }
|
|
29
|
+
catch { body = null; }
|
|
30
|
+
const failure = inspectAppLoad(body, response.status);
|
|
31
|
+
if (failure) throw new AppLoadError('Marketplace initialization failed.', failure);
|
|
32
|
+
return appLoadResult(body, response.status);
|
|
33
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { AppLoadFailure, AppLoadResponse, AppLoadResult } from '../../interfaces/orionplace/transport';
|
|
2
|
+
|
|
3
|
+
const record = (value: unknown): value is Record<string, unknown> =>
|
|
4
|
+
value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
|
|
6
|
+
/** Validate the bootstrap boundary. Interfaces describe nested data, not a runtime schema. */
|
|
7
|
+
export function inspectAppLoad(body: unknown, httpStatus: number): AppLoadFailure | null {
|
|
8
|
+
if (httpStatus < 200 || httpStatus >= 300) return { kind: 'http', httpStatus, fields: [] };
|
|
9
|
+
if (record(body) && record(body.form_errors) && Object.keys(body.form_errors).length) {
|
|
10
|
+
return { kind: 'validation', httpStatus, fields: Object.keys(body.form_errors) };
|
|
11
|
+
}
|
|
12
|
+
const page = record(body) && body.page_data;
|
|
13
|
+
const init = record(page) && page.init;
|
|
14
|
+
const unauth = record(page) && page.unauth;
|
|
15
|
+
if (!record(body) || !('form_errors' in body) || (body.form_errors !== null && !record(body.form_errors)) ||
|
|
16
|
+
!record(init) || typeof init.is_authenticated !== 'boolean' ||
|
|
17
|
+
!record(init.locale) || !record(init.currencies) || !record(unauth) || !record(unauth.paths) ||
|
|
18
|
+
Object.values(unauth.paths).some(group => !record(group) || Object.values(group).some(endpoint =>
|
|
19
|
+
!record(endpoint) || typeof endpoint.target !== 'string' || !Array.isArray(endpoint.methods)))) {
|
|
20
|
+
return { kind: 'invalid-response', httpStatus, fields: [] };
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function appLoadResult(body: unknown, httpStatus: number): AppLoadResult {
|
|
26
|
+
const failure = inspectAppLoad(body, httpStatus);
|
|
27
|
+
if (failure) throw new Error(`AppLoad: ${failure.kind}`);
|
|
28
|
+
return {
|
|
29
|
+
response: body as AppLoadResponse & { page_data: NonNullable<AppLoadResponse['page_data']> },
|
|
30
|
+
httpStatus,
|
|
31
|
+
loadedAt: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Transport and envelope failures reach the store as this sanitized shape. */
|
|
36
|
+
export function appLoadFailure(error: unknown): AppLoadFailure {
|
|
37
|
+
const detail = error && typeof error === 'object' && 'data' in error ? (error as { data: unknown }).data : null;
|
|
38
|
+
const known = record(detail) && 'kind' in detail &&
|
|
39
|
+
['http', 'validation', 'invalid-response', 'network'].includes(String(detail.kind));
|
|
40
|
+
if (!known || !record(detail)) return { kind: 'network', httpStatus: null, fields: [] };
|
|
41
|
+
return {
|
|
42
|
+
kind: detail.kind as AppLoadFailure['kind'],
|
|
43
|
+
httpStatus: typeof detail.httpStatus === 'number' ? detail.httpStatus : null,
|
|
44
|
+
fields: Array.isArray(detail.fields) ? detail.fields.filter((field): field is string => typeof field === 'string') : [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Carries a sanitized failure from the transport to the store without a stack or response body. */
|
|
49
|
+
export class AppLoadError extends Error {
|
|
50
|
+
readonly data: AppLoadFailure;
|
|
51
|
+
constructor(message: string, data: AppLoadFailure) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = 'AppLoadError';
|
|
54
|
+
this.data = data;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { cache } from 'react';
|
|
2
|
+
import { headers } from 'next/headers';
|
|
3
|
+
import { forwardMarketplace } from '__ORIONPLACE_RUNTIME__';
|
|
4
|
+
import { appLoadResult, inspectAppLoad } from './appLoadResult';
|
|
5
|
+
import type { AppLoadFailure, AppLoadRequest, AppLoadResult } from '../../interfaces/orionplace/transport';
|
|
6
|
+
|
|
7
|
+
/** Server-side bootstrap. Never import this module from a client component. */
|
|
8
|
+
export interface ServerAppLoad {
|
|
9
|
+
result: AppLoadResult | null;
|
|
10
|
+
failure: AppLoadFailure | null;
|
|
11
|
+
/** Raw backend `Set-Cookie` headers; only a route handler or server action may relay them. */
|
|
12
|
+
cookies: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Headers a server render forwards to the marketplace gateway. */
|
|
16
|
+
const FORWARDED = [
|
|
17
|
+
'cookie', 'x-front-domain', 'x-forwarded-host', 'x-forwarded-proto', 'host',
|
|
18
|
+
'accept-language', 'user-agent', 'x-init-ip', 'x-real-ip', 'x-forwarded-for', 'x-tz',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
async function buildRequest(signal: AbortSignal): Promise<Request> {
|
|
22
|
+
const incoming = await headers();
|
|
23
|
+
const host = incoming.get('x-forwarded-host') || incoming.get('host') || 'localhost';
|
|
24
|
+
const protocol = incoming.get('x-forwarded-proto')?.split(',')[0]?.trim() || 'https';
|
|
25
|
+
const url = new URL('/api/appload', `${protocol}://${host}`);
|
|
26
|
+
const forward = new Headers();
|
|
27
|
+
for (const name of FORWARDED) {
|
|
28
|
+
const value = incoming.get(name);
|
|
29
|
+
if (value) forward.set(name, value);
|
|
30
|
+
}
|
|
31
|
+
forward.set('accept', 'application/json');
|
|
32
|
+
forward.set('content-type', 'application/json');
|
|
33
|
+
// Same-origin server request: the gateway rejects cross-site mutations by Origin.
|
|
34
|
+
forward.set('origin', url.origin);
|
|
35
|
+
return new Request(url, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: forward,
|
|
38
|
+
body: JSON.stringify({ detalize_mode: 'light', ignore_cache: true } satisfies AppLoadRequest),
|
|
39
|
+
signal,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calls the same gateway the browser uses, in process, with this request's domain
|
|
45
|
+
* and session. It never throws: the failure is stored for the interface to handle.
|
|
46
|
+
*/
|
|
47
|
+
export async function requestAppLoad(signal?: AbortSignal): Promise<ServerAppLoad> {
|
|
48
|
+
const timeout = AbortSignal.timeout(35_000);
|
|
49
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
50
|
+
let response: Response;
|
|
51
|
+
try {
|
|
52
|
+
response = await forwardMarketplace(await buildRequest(combined), {
|
|
53
|
+
mimicFrontDomain: process.env.MIMIC_FRONT_DOMAIN ?? '',
|
|
54
|
+
});
|
|
55
|
+
} catch {
|
|
56
|
+
return { result: null, failure: { kind: 'network', httpStatus: null, fields: [] }, cookies: [] };
|
|
57
|
+
}
|
|
58
|
+
const cookies = response.headers.getSetCookie();
|
|
59
|
+
let body: unknown = null;
|
|
60
|
+
try { body = await response.json(); }
|
|
61
|
+
catch { body = null; }
|
|
62
|
+
const failure = inspectAppLoad(body, response.status);
|
|
63
|
+
if (failure) return { result: null, failure, cookies };
|
|
64
|
+
return { result: appLoadResult(body, response.status), failure: null, cookies };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* One request per server render, shared by the layout and any server component
|
|
69
|
+
* that awaits it. Reading it in a layout initializes the marketplace before the
|
|
70
|
+
* page renders; it never opens an authentication prompt.
|
|
71
|
+
*/
|
|
72
|
+
export const getOrionplaceAppLoad = cache((): Promise<ServerAppLoad> => requestAppLoad());
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { headers } from 'next/headers';
|
|
2
|
+
import { resolveFrontDomain } from '__ORIONPLACE_RUNTIME__';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The storefront domain selected for this request: `X-Front-Domain`,
|
|
6
|
+
* `X-Forwarded-Host` or `Host`, and `MIMIC_FRONT_DOMAIN` on localhost.
|
|
7
|
+
* Use it for canonical URLs and page metadata; it is never a fixed site URL.
|
|
8
|
+
*/
|
|
9
|
+
export async function getStorefrontDomain(): Promise<string | null> {
|
|
10
|
+
const incoming = await headers();
|
|
11
|
+
const forward = new Headers();
|
|
12
|
+
for (const [name, value] of incoming.entries()) forward.set(name, value);
|
|
13
|
+
const host = forward.get('x-forwarded-host') || forward.get('host') || 'localhost';
|
|
14
|
+
return resolveFrontDomain(new Request(`https://${host}/`, { headers: forward }),
|
|
15
|
+
process.env.MIMIC_FRONT_DOMAIN ?? '');
|
|
16
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useRef, type ReactNode } from 'react';
|
|
4
|
+
import { useStore } from 'zustand';
|
|
5
|
+
import {
|
|
6
|
+
createOrionplaceStore, initialSnapshot,
|
|
7
|
+
type OrionplaceState, type OrionplaceStore,
|
|
8
|
+
} from '../stores/orionplace';
|
|
9
|
+
import type { AppLoadFailure, AppLoadResult } from '../interfaces/orionplace';
|
|
10
|
+
|
|
11
|
+
const OrionplaceContext = createContext<OrionplaceStore | null>(null);
|
|
12
|
+
|
|
13
|
+
export interface OrionplaceProviderProps {
|
|
14
|
+
/** The server render's snapshot. Hydration reuses it instead of loading again. */
|
|
15
|
+
result?: AppLoadResult | null;
|
|
16
|
+
failure?: AppLoadFailure | null;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Creates one store for this browser session and keeps it across re-renders. */
|
|
21
|
+
export function OrionplaceProvider({ result = null, failure = null, children }: OrionplaceProviderProps) {
|
|
22
|
+
const store = useRef<OrionplaceStore>(null);
|
|
23
|
+
store.current ??= createOrionplaceStore(initialSnapshot(result, failure));
|
|
24
|
+
return <OrionplaceContext.Provider value={store.current}>{children}</OrionplaceContext.Provider>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function useStoreApi(): OrionplaceStore {
|
|
28
|
+
const store = useContext(OrionplaceContext);
|
|
29
|
+
if (!store) throw new Error('Render this component inside <OrionplaceProvider>.');
|
|
30
|
+
return store;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Subscribes to one selected part of the marketplace state. */
|
|
34
|
+
export function useOrionplace<T>(selector: (state: OrionplaceState) => T): T {
|
|
35
|
+
return useStore(useStoreApi(), selector);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The store itself, for actions and one-off reads outside render. */
|
|
39
|
+
export const useOrionplaceStore = useStoreApi;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Personalized pages are never shared between visitors or storefront domains.
|
|
5
|
+
* Next.js manages its own `Vary` header for pages; the API gateway sets the
|
|
6
|
+
* storefront-domain `Vary` on its own responses.
|
|
7
|
+
*/
|
|
8
|
+
export function proxy() {
|
|
9
|
+
const response = NextResponse.next();
|
|
10
|
+
response.headers.set('Cache-Control', 'private, no-store');
|
|
11
|
+
return response;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// The gateway sets its own headers for /api; static assets keep their caching.
|
|
15
|
+
export const config = {
|
|
16
|
+
matcher: ['/((?!api/|_next/static|_next/image|favicon.ico).*)'],
|
|
17
|
+
};
|