@4alldigital/foundation-ui--core 3.4.1 → 3.5.1
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/package.json +2 -2
- package/src/components/AddressForm/AddressForm.tsx +211 -0
- package/src/components/AddressForm/AddressForm.types.ts +29 -0
- package/src/components/AddressForm/index.ts +2 -0
- package/src/components/ProductDetail/ProductDetail.tsx +20 -2
- package/src/components/ProductDetail/ProductDetail.types.ts +3 -1
- package/src/components/index.ts +2 -0
- package/src/templates/OrderDetailScreen/OrderDetailScreen.tsx +188 -0
- package/src/templates/OrderDetailScreen/OrderDetailScreen.types.ts +39 -0
- package/src/templates/OrderDetailScreen/index.ts +2 -0
- package/src/templates/OrdersHistoryScreen/OrdersHistoryScreen.tsx +132 -0
- package/src/templates/OrdersHistoryScreen/OrdersHistoryScreen.types.ts +22 -0
- package/src/templates/OrdersHistoryScreen/index.ts +2 -0
- package/src/templates/PurchaseConfirmationScreen/PurchaseConfirmationScreen.stories.tsx +66 -0
- package/src/templates/PurchaseConfirmationScreen/PurchaseConfirmationScreen.tsx +144 -0
- package/src/templates/PurchaseConfirmationScreen/PurchaseConfirmationScreen.types.ts +30 -0
- package/src/templates/PurchaseConfirmationScreen/index.ts +1 -0
- package/src/templates/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4alldigital/foundation-ui--core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1",
|
|
4
4
|
"description": "Foundation UI Core Component Library (source distribution)",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -32,5 +32,5 @@
|
|
|
32
32
|
},
|
|
33
33
|
"author": "Joe Mewes",
|
|
34
34
|
"license": "MIT",
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "e8ca79ab96c86064d214da737e4d87df759c1d29"
|
|
36
36
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { cn } from '../../utils';
|
|
4
|
+
import { AddressFormProps, ShippingAddress, AddressErrors } from './AddressForm.types';
|
|
5
|
+
import TextInput from '../TextInput';
|
|
6
|
+
import FormSelect from '../FormSelect';
|
|
7
|
+
|
|
8
|
+
// Common countries
|
|
9
|
+
const COUNTRIES = [
|
|
10
|
+
{ value: 'GB', label: 'United Kingdom' },
|
|
11
|
+
{ value: 'US', label: 'United States' },
|
|
12
|
+
{ value: 'CA', label: 'Canada' },
|
|
13
|
+
{ value: 'AU', label: 'Australia' },
|
|
14
|
+
{ value: 'DE', label: 'Germany' },
|
|
15
|
+
{ value: 'FR', label: 'France' },
|
|
16
|
+
{ value: 'ES', label: 'Spain' },
|
|
17
|
+
{ value: 'IT', label: 'Italy' },
|
|
18
|
+
{ value: 'NL', label: 'Netherlands' },
|
|
19
|
+
{ value: 'IE', label: 'Ireland' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const AddressForm = React.forwardRef<HTMLDivElement, AddressFormProps>(
|
|
23
|
+
({ value = {}, onChange, onValidationChange, disabled = false, className, testID }, ref) => {
|
|
24
|
+
const [address, setAddress] = useState<Partial<ShippingAddress>>(value);
|
|
25
|
+
const [errors, setErrors] = useState<AddressErrors>({});
|
|
26
|
+
const [touched, setTouched] = useState<Record<string, boolean>>({});
|
|
27
|
+
|
|
28
|
+
// Validate field
|
|
29
|
+
const validateField = (name: keyof ShippingAddress, val: string): string | undefined => {
|
|
30
|
+
if (!val && name !== 'line2' && name !== 'phone') {
|
|
31
|
+
return 'This field is required';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (name === 'postal_code' && val) {
|
|
35
|
+
// Basic postal code validation
|
|
36
|
+
if (val.length < 3) {
|
|
37
|
+
return 'Invalid postal code';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (name === 'phone' && val) {
|
|
42
|
+
// Basic phone validation
|
|
43
|
+
if (val.length < 10) {
|
|
44
|
+
return 'Invalid phone number';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Validate all fields
|
|
52
|
+
const validateForm = (): boolean => {
|
|
53
|
+
const newErrors: AddressErrors = {};
|
|
54
|
+
let isValid = true;
|
|
55
|
+
|
|
56
|
+
(['name', 'line1', 'city', 'state', 'postal_code', 'country'] as const).forEach((field) => {
|
|
57
|
+
const error = validateField(field, address[field] || '');
|
|
58
|
+
if (error) {
|
|
59
|
+
newErrors[field] = error;
|
|
60
|
+
isValid = false;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
setErrors(newErrors);
|
|
65
|
+
return isValid;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Handle field change
|
|
69
|
+
const handleChange = (name: keyof ShippingAddress, newValue: string) => {
|
|
70
|
+
const updated = { ...address, [name]: newValue };
|
|
71
|
+
setAddress(updated);
|
|
72
|
+
|
|
73
|
+
// Validate on change if already touched
|
|
74
|
+
if (touched[name]) {
|
|
75
|
+
const error = validateField(name, newValue);
|
|
76
|
+
setErrors((prev) => ({ ...prev, [name]: error }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if form is complete and valid
|
|
80
|
+
if (
|
|
81
|
+
updated.name &&
|
|
82
|
+
updated.line1 &&
|
|
83
|
+
updated.city &&
|
|
84
|
+
updated.state &&
|
|
85
|
+
updated.postal_code &&
|
|
86
|
+
updated.country
|
|
87
|
+
) {
|
|
88
|
+
onChange(updated as ShippingAddress);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Handle field blur
|
|
93
|
+
const handleBlur = (name: keyof ShippingAddress) => {
|
|
94
|
+
setTouched((prev) => ({ ...prev, [name]: true }));
|
|
95
|
+
const error = validateField(name, address[name] || '');
|
|
96
|
+
setErrors((prev) => ({ ...prev, [name]: error }));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// Notify validation state
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const isValid = validateForm();
|
|
102
|
+
onValidationChange?.(isValid);
|
|
103
|
+
}, [address, onValidationChange]);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div ref={ref} data-testid={testID || 'AddressForm'} className={cn('space-y-4', className)}>
|
|
107
|
+
<h3 className="text-lg font-semibold">Shipping Address</h3>
|
|
108
|
+
|
|
109
|
+
{/* Full Name */}
|
|
110
|
+
<TextInput
|
|
111
|
+
id="shipping-name"
|
|
112
|
+
placeholder="Full Name"
|
|
113
|
+
value={address.name || ''}
|
|
114
|
+
onChange={(e) => handleChange('name', e.target.value)}
|
|
115
|
+
onBlur={() => handleBlur('name')}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
error={touched.name && errors.name}
|
|
118
|
+
required
|
|
119
|
+
/>
|
|
120
|
+
|
|
121
|
+
{/* Address Line 1 */}
|
|
122
|
+
<TextInput
|
|
123
|
+
id="shipping-line1"
|
|
124
|
+
placeholder="Street Address"
|
|
125
|
+
value={address.line1 || ''}
|
|
126
|
+
onChange={(e) => handleChange('line1', e.target.value)}
|
|
127
|
+
onBlur={() => handleBlur('line1')}
|
|
128
|
+
disabled={disabled}
|
|
129
|
+
error={touched.line1 && errors.line1}
|
|
130
|
+
required
|
|
131
|
+
/>
|
|
132
|
+
|
|
133
|
+
{/* Address Line 2 */}
|
|
134
|
+
<TextInput
|
|
135
|
+
id="shipping-line2"
|
|
136
|
+
placeholder="Apartment, suite, etc. (optional)"
|
|
137
|
+
value={address.line2 || ''}
|
|
138
|
+
onChange={(e) => handleChange('line2', e.target.value)}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
/>
|
|
141
|
+
|
|
142
|
+
{/* City and State */}
|
|
143
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
144
|
+
<TextInput
|
|
145
|
+
id="shipping-city"
|
|
146
|
+
placeholder="City"
|
|
147
|
+
value={address.city || ''}
|
|
148
|
+
onChange={(e) => handleChange('city', e.target.value)}
|
|
149
|
+
onBlur={() => handleBlur('city')}
|
|
150
|
+
disabled={disabled}
|
|
151
|
+
error={touched.city && errors.city}
|
|
152
|
+
required
|
|
153
|
+
/>
|
|
154
|
+
|
|
155
|
+
<TextInput
|
|
156
|
+
id="shipping-state"
|
|
157
|
+
placeholder="State / County / Province"
|
|
158
|
+
value={address.state || ''}
|
|
159
|
+
onChange={(e) => handleChange('state', e.target.value)}
|
|
160
|
+
onBlur={() => handleBlur('state')}
|
|
161
|
+
disabled={disabled}
|
|
162
|
+
error={touched.state && errors.state}
|
|
163
|
+
required
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Postal Code and Country */}
|
|
168
|
+
<div className="grid gap-4 sm:grid-cols-2">
|
|
169
|
+
<TextInput
|
|
170
|
+
id="shipping-postal-code"
|
|
171
|
+
placeholder="Postal / ZIP Code"
|
|
172
|
+
value={address.postal_code || ''}
|
|
173
|
+
onChange={(e) => handleChange('postal_code', e.target.value)}
|
|
174
|
+
onBlur={() => handleBlur('postal_code')}
|
|
175
|
+
disabled={disabled}
|
|
176
|
+
error={touched.postal_code && errors.postal_code}
|
|
177
|
+
required
|
|
178
|
+
/>
|
|
179
|
+
|
|
180
|
+
<FormSelect
|
|
181
|
+
id="shipping-country"
|
|
182
|
+
placeholder="Country"
|
|
183
|
+
value={address.country || ''}
|
|
184
|
+
onChange={(value) => handleChange('country', value)}
|
|
185
|
+
onBlur={() => handleBlur('country')}
|
|
186
|
+
options={COUNTRIES}
|
|
187
|
+
disabled={disabled}
|
|
188
|
+
error={touched.country && errors.country}
|
|
189
|
+
required
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
{/* Phone */}
|
|
194
|
+
<TextInput
|
|
195
|
+
id="shipping-phone"
|
|
196
|
+
type="tel"
|
|
197
|
+
placeholder="Phone Number (optional)"
|
|
198
|
+
value={address.phone || ''}
|
|
199
|
+
onChange={(e) => handleChange('phone', e.target.value)}
|
|
200
|
+
onBlur={() => handleBlur('phone')}
|
|
201
|
+
disabled={disabled}
|
|
202
|
+
error={touched.phone && errors.phone}
|
|
203
|
+
/>
|
|
204
|
+
</div>
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
AddressForm.displayName = 'AddressForm';
|
|
210
|
+
|
|
211
|
+
export type { AddressFormProps, ShippingAddress };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface ShippingAddress {
|
|
2
|
+
name: string;
|
|
3
|
+
line1: string;
|
|
4
|
+
line2?: string;
|
|
5
|
+
city: string;
|
|
6
|
+
state: string;
|
|
7
|
+
postal_code: string;
|
|
8
|
+
country: string;
|
|
9
|
+
phone?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface AddressFormProps {
|
|
13
|
+
value?: Partial<ShippingAddress>;
|
|
14
|
+
onChange: (address: ShippingAddress) => void;
|
|
15
|
+
onValidationChange?: (isValid: boolean) => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
testID?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface AddressErrors {
|
|
22
|
+
name?: string;
|
|
23
|
+
line1?: string;
|
|
24
|
+
city?: string;
|
|
25
|
+
state?: string;
|
|
26
|
+
postal_code?: string;
|
|
27
|
+
country?: string;
|
|
28
|
+
phone?: string;
|
|
29
|
+
}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '../ShadcnCarousel';
|
|
15
15
|
import Image from '../Image';
|
|
16
16
|
import Tabs from '../Tabs';
|
|
17
|
+
import { AddressForm, type ShippingAddress } from '../AddressForm';
|
|
17
18
|
import { Minus, Plus, ShoppingCart } from 'lucide-react';
|
|
18
19
|
|
|
19
20
|
export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps>(
|
|
@@ -21,6 +22,8 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
|
|
|
21
22
|
const [selectedSize, setSelectedSize] = useState<string | undefined>();
|
|
22
23
|
const [selectedColor, setSelectedColor] = useState<string | undefined>();
|
|
23
24
|
const [quantity, setQuantity] = useState(1);
|
|
25
|
+
const [shippingAddress, setShippingAddress] = useState<ShippingAddress | undefined>();
|
|
26
|
+
const [isShippingValid, setIsShippingValid] = useState(false);
|
|
24
27
|
|
|
25
28
|
const formatPrice = (price: number, currency: string) => {
|
|
26
29
|
return new Intl.NumberFormat('en-GB', {
|
|
@@ -70,6 +73,7 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
|
|
|
70
73
|
selectedSize,
|
|
71
74
|
selectedColor,
|
|
72
75
|
quantity,
|
|
76
|
+
shippingAddress,
|
|
73
77
|
};
|
|
74
78
|
onPurchase(state);
|
|
75
79
|
};
|
|
@@ -78,7 +82,8 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
|
|
|
78
82
|
const canPurchase =
|
|
79
83
|
!isLoading &&
|
|
80
84
|
(sizeOptions.length === 0 || selectedSize) &&
|
|
81
|
-
(colorOptions.length === 0 || selectedColor)
|
|
85
|
+
(colorOptions.length === 0 || selectedColor) &&
|
|
86
|
+
(!isAuthenticated || (isAuthenticated && isShippingValid));
|
|
82
87
|
|
|
83
88
|
// Tab content
|
|
84
89
|
const tabItems = [
|
|
@@ -205,6 +210,18 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
|
|
|
205
210
|
</div>
|
|
206
211
|
)}
|
|
207
212
|
|
|
213
|
+
{/* Shipping Address Form (only for authenticated users) */}
|
|
214
|
+
{isAuthenticated && (
|
|
215
|
+
<div className="border-t pt-6">
|
|
216
|
+
<AddressForm
|
|
217
|
+
value={shippingAddress}
|
|
218
|
+
onChange={setShippingAddress}
|
|
219
|
+
onValidationChange={setIsShippingValid}
|
|
220
|
+
disabled={isLoading}
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
|
|
208
225
|
{/* Purchase Button or Auth Message */}
|
|
209
226
|
{!isAuthenticated ? (
|
|
210
227
|
<div className="rounded-lg border border-primary bg-primary/10 p-6 text-center">
|
|
@@ -230,7 +247,8 @@ export const ProductDetail = React.forwardRef<HTMLDivElement, ProductDetailProps
|
|
|
230
247
|
{!canPurchase && !isLoading && (
|
|
231
248
|
<p className="text-sm text-muted-foreground">
|
|
232
249
|
{!selectedSize && sizeOptions.length > 0 && 'Please select a size. '}
|
|
233
|
-
{!selectedColor && colorOptions.length > 0 && 'Please select a color.'}
|
|
250
|
+
{!selectedColor && colorOptions.length > 0 && 'Please select a color. '}
|
|
251
|
+
{!isShippingValid && 'Please complete shipping address.'}
|
|
234
252
|
</p>
|
|
235
253
|
)}
|
|
236
254
|
</>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Product } from '../ProductCard
|
|
1
|
+
import { Product } from '../ProductCard';
|
|
2
|
+
import { ShippingAddress } from '../AddressForm';
|
|
2
3
|
|
|
3
4
|
export interface ProductDetailState {
|
|
4
5
|
selectedSize?: string;
|
|
5
6
|
selectedColor?: string;
|
|
6
7
|
quantity: number;
|
|
8
|
+
shippingAddress?: ShippingAddress;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
11
|
export interface ProductDetailProps {
|
package/src/components/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export { default as Avatar } from './Avatar';
|
|
|
25
25
|
export { default as Banner } from './Banner';
|
|
26
26
|
|
|
27
27
|
// MOLECULES
|
|
28
|
+
export { AddressForm } from './AddressForm';
|
|
28
29
|
export { default as ButtonGroup } from './ButtonGroup';
|
|
29
30
|
export { default as Card } from './Card';
|
|
30
31
|
export { default as DisplayHeading } from './DisplayHeading';
|
|
@@ -155,6 +156,7 @@ export type { Props as HeaderProps } from './Header/Header.types';
|
|
|
155
156
|
export type { ShadcnButtonProps } from './ShadcnButton';
|
|
156
157
|
|
|
157
158
|
// PRODUCT COMPONENT TYPES
|
|
159
|
+
export type { AddressFormProps, ShippingAddress } from './AddressForm';
|
|
158
160
|
export type { ProductCardProps, ProductCardVariantProps, Product } from './ProductCard';
|
|
159
161
|
export type { VariantSelectorProps, VariantOption } from './VariantSelector';
|
|
160
162
|
export type { ProductDetailProps, ProductDetailState } from './ProductDetail';
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Screen from '../../components/Screen';
|
|
3
|
+
import DisplayHeading from '../../components/DisplayHeading';
|
|
4
|
+
import { format } from 'date-fns';
|
|
5
|
+
import { OrderDetailScreenProps } from './OrderDetailScreen.types';
|
|
6
|
+
|
|
7
|
+
const OrderDetailScreen = ({
|
|
8
|
+
testID,
|
|
9
|
+
order,
|
|
10
|
+
isLoading = false,
|
|
11
|
+
error,
|
|
12
|
+
onBackClick,
|
|
13
|
+
}: OrderDetailScreenProps) => {
|
|
14
|
+
const formatPrice = (amount: number, currency: string) => {
|
|
15
|
+
return new Intl.NumberFormat('en-US', {
|
|
16
|
+
style: 'currency',
|
|
17
|
+
currency: currency.toUpperCase(),
|
|
18
|
+
}).format(amount / 100);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const formatStatus = (status: string) => {
|
|
22
|
+
return status
|
|
23
|
+
.split('_')
|
|
24
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
25
|
+
.join(' ');
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getStatusColor = (status: string) => {
|
|
29
|
+
switch (status) {
|
|
30
|
+
case 'succeeded':
|
|
31
|
+
return 'bg-success/10 text-success';
|
|
32
|
+
case 'processing':
|
|
33
|
+
return 'bg-warning/10 text-warning';
|
|
34
|
+
case 'requires_payment_method':
|
|
35
|
+
case 'requires_confirmation':
|
|
36
|
+
case 'requires_action':
|
|
37
|
+
return 'bg-warning/10 text-warning';
|
|
38
|
+
case 'canceled':
|
|
39
|
+
return 'bg-muted text-muted-foreground';
|
|
40
|
+
default:
|
|
41
|
+
return 'bg-muted text-body-text';
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Screen data-testid={testID || 'OrderDetailScreen'}>
|
|
47
|
+
<div className="container mx-auto px-4 py-8">
|
|
48
|
+
{onBackClick && (
|
|
49
|
+
<button onClick={onBackClick} className="mb-8 text-primary hover:underline">
|
|
50
|
+
← Back to Orders
|
|
51
|
+
</button>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
{isLoading && (
|
|
55
|
+
<div className="flex items-center justify-center py-16">
|
|
56
|
+
<div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
|
|
60
|
+
{error && (
|
|
61
|
+
<div className="rounded-lg bg-destructive/10 p-8 text-center">
|
|
62
|
+
<h2 className="mb-2 text-2xl font-bold text-destructive">Unable to Load Order</h2>
|
|
63
|
+
<p className="text-muted-foreground">{error.message || 'Please try again later.'}</p>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
{!isLoading && !error && order && (
|
|
68
|
+
<div className="space-y-6">
|
|
69
|
+
{/* Order Header */}
|
|
70
|
+
<div className="rounded-lg border border-border bg-card p-6">
|
|
71
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
|
72
|
+
<div>
|
|
73
|
+
<h2 className="mb-2 text-2xl font-bold">
|
|
74
|
+
Order #{order.id.slice(-8).toUpperCase()}
|
|
75
|
+
</h2>
|
|
76
|
+
<p className="text-muted-foreground">
|
|
77
|
+
Placed on {format(new Date(order.created * 1000), 'MMMM d, yyyy')}
|
|
78
|
+
</p>
|
|
79
|
+
</div>
|
|
80
|
+
<div
|
|
81
|
+
className={`inline-block rounded-full px-4 py-2 text-sm font-medium ${getStatusColor(order.status)}`}
|
|
82
|
+
>
|
|
83
|
+
{formatStatus(order.status)}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
{/* Product Details */}
|
|
89
|
+
<div className="rounded-lg border border-border bg-card p-6">
|
|
90
|
+
<h3 className="mb-4 text-xl font-semibold">Product Details</h3>
|
|
91
|
+
<div className="space-y-3">
|
|
92
|
+
<div className="flex justify-between">
|
|
93
|
+
<span className="text-muted-foreground">Product</span>
|
|
94
|
+
<span className="font-medium">{order.productName}</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="flex justify-between">
|
|
97
|
+
<span className="text-muted-foreground">Quantity</span>
|
|
98
|
+
<span className="font-medium">{order.quantity}</span>
|
|
99
|
+
</div>
|
|
100
|
+
{order.selectedVariants && Object.keys(order.selectedVariants).length > 0 && (
|
|
101
|
+
<div className="flex justify-between">
|
|
102
|
+
<span className="text-muted-foreground">Options</span>
|
|
103
|
+
<span className="font-medium">
|
|
104
|
+
{Object.entries(order.selectedVariants)
|
|
105
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
106
|
+
.join(', ')}
|
|
107
|
+
</span>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
<div className="flex justify-between border-t pt-3">
|
|
111
|
+
<span className="font-medium">Total</span>
|
|
112
|
+
<span className="text-2xl font-bold">
|
|
113
|
+
{formatPrice(order.amount, order.currency)}
|
|
114
|
+
</span>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{/* Shipping Address */}
|
|
120
|
+
{order.shipping && (
|
|
121
|
+
<div className="rounded-lg border border-border bg-card p-6">
|
|
122
|
+
<h3 className="mb-4 text-xl font-semibold">Shipping Address</h3>
|
|
123
|
+
<div className="space-y-1">
|
|
124
|
+
<p className="font-medium">{order.shipping.name}</p>
|
|
125
|
+
<p>{order.shipping.address.line1}</p>
|
|
126
|
+
{order.shipping.address.line2 && <p>{order.shipping.address.line2}</p>}
|
|
127
|
+
<p>
|
|
128
|
+
{order.shipping.address.city}, {order.shipping.address.state}{' '}
|
|
129
|
+
{order.shipping.address.postal_code}
|
|
130
|
+
</p>
|
|
131
|
+
<p>{order.shipping.address.country}</p>
|
|
132
|
+
{order.shipping.phone && (
|
|
133
|
+
<p className="mt-2 text-muted-foreground">Phone: {order.shipping.phone}</p>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{/* Tracking Information */}
|
|
140
|
+
{(order.trackingNumber || order.trackingUrl) && (
|
|
141
|
+
<div className="rounded-lg border border-border bg-card p-6">
|
|
142
|
+
<h3 className="mb-4 text-xl font-semibold">Tracking Information</h3>
|
|
143
|
+
<div className="space-y-3">
|
|
144
|
+
{order.trackingNumber && (
|
|
145
|
+
<div className="flex justify-between">
|
|
146
|
+
<span className="text-muted-foreground">Tracking Number</span>
|
|
147
|
+
<span className="font-medium">{order.trackingNumber}</span>
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
{order.trackingUrl && (
|
|
151
|
+
<a
|
|
152
|
+
href={order.trackingUrl}
|
|
153
|
+
target="_blank"
|
|
154
|
+
rel="noopener noreferrer"
|
|
155
|
+
className="block w-full rounded-md bg-primary px-6 py-2 text-center text-white hover:bg-primary/90"
|
|
156
|
+
>
|
|
157
|
+
Track Package
|
|
158
|
+
</a>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
)}
|
|
163
|
+
|
|
164
|
+
{/* Receipt */}
|
|
165
|
+
{order.receiptUrl && (
|
|
166
|
+
<div className="rounded-lg border border-border bg-card p-6">
|
|
167
|
+
<h3 className="mb-4 text-xl font-semibold">Receipt</h3>
|
|
168
|
+
<a
|
|
169
|
+
href={order.receiptUrl}
|
|
170
|
+
target="_blank"
|
|
171
|
+
rel="noopener noreferrer"
|
|
172
|
+
className="block w-full rounded-md border border-border bg-secondary px-6 py-2 text-center hover:bg-secondary/80"
|
|
173
|
+
>
|
|
174
|
+
View Receipt
|
|
175
|
+
</a>
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
</Screen>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
OrderDetailScreen.displayName = 'OrderDetailScreen';
|
|
186
|
+
|
|
187
|
+
export default OrderDetailScreen;
|
|
188
|
+
export type { OrderDetailScreenProps };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export interface ShippingAddress {
|
|
2
|
+
line1: string;
|
|
3
|
+
line2?: string;
|
|
4
|
+
city: string;
|
|
5
|
+
state: string;
|
|
6
|
+
postal_code: string;
|
|
7
|
+
country: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Shipping {
|
|
11
|
+
name: string;
|
|
12
|
+
phone?: string;
|
|
13
|
+
address: ShippingAddress;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OrderDetail {
|
|
17
|
+
id: string;
|
|
18
|
+
status: string;
|
|
19
|
+
amount: number;
|
|
20
|
+
currency: string;
|
|
21
|
+
created: number;
|
|
22
|
+
productId: string;
|
|
23
|
+
productName: string;
|
|
24
|
+
quantity: string;
|
|
25
|
+
partnerId?: string;
|
|
26
|
+
selectedVariants?: Record<string, string>;
|
|
27
|
+
shipping?: Shipping;
|
|
28
|
+
trackingNumber?: string;
|
|
29
|
+
trackingUrl?: string;
|
|
30
|
+
receiptUrl?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface OrderDetailScreenProps {
|
|
34
|
+
testID?: string;
|
|
35
|
+
order?: OrderDetail;
|
|
36
|
+
isLoading?: boolean;
|
|
37
|
+
error?: Error | null;
|
|
38
|
+
onBackClick?: () => void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Screen from '../../components/Screen';
|
|
3
|
+
import DisplayHeading from '../../components/DisplayHeading';
|
|
4
|
+
import { formatDistance } from 'date-fns';
|
|
5
|
+
import { OrdersHistoryScreenProps } from './OrdersHistoryScreen.types';
|
|
6
|
+
|
|
7
|
+
const OrdersHistoryScreen = ({
|
|
8
|
+
testID,
|
|
9
|
+
orders = [],
|
|
10
|
+
isLoading = false,
|
|
11
|
+
error,
|
|
12
|
+
onOrderClick,
|
|
13
|
+
}: OrdersHistoryScreenProps) => {
|
|
14
|
+
const formatPrice = (amount: number, currency: string) => {
|
|
15
|
+
return new Intl.NumberFormat('en-US', {
|
|
16
|
+
style: 'currency',
|
|
17
|
+
currency: currency.toUpperCase(),
|
|
18
|
+
}).format(amount / 100);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const formatStatus = (status: string) => {
|
|
22
|
+
return status
|
|
23
|
+
.split('_')
|
|
24
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
25
|
+
.join(' ');
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const getStatusColor = (status: string) => {
|
|
29
|
+
switch (status) {
|
|
30
|
+
case 'succeeded':
|
|
31
|
+
return 'text-success';
|
|
32
|
+
case 'processing':
|
|
33
|
+
return 'text-warning';
|
|
34
|
+
case 'requires_payment_method':
|
|
35
|
+
case 'requires_confirmation':
|
|
36
|
+
case 'requires_action':
|
|
37
|
+
return 'text-warning';
|
|
38
|
+
case 'canceled':
|
|
39
|
+
return 'text-muted-foreground';
|
|
40
|
+
default:
|
|
41
|
+
return 'text-body-text';
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Screen data-testid={testID || 'OrdersHistoryScreen'}>
|
|
47
|
+
<DisplayHeading text="Order History" />
|
|
48
|
+
|
|
49
|
+
<div className="container mx-auto px-4 py-8">
|
|
50
|
+
{isLoading && (
|
|
51
|
+
<div className="flex items-center justify-center py-16">
|
|
52
|
+
<div className="h-12 w-12 animate-spin rounded-full border-4 border-primary border-t-transparent"></div>
|
|
53
|
+
</div>
|
|
54
|
+
)}
|
|
55
|
+
|
|
56
|
+
{error && (
|
|
57
|
+
<div className="rounded-lg bg-destructive/10 p-8 text-center">
|
|
58
|
+
<h2 className="mb-2 text-2xl font-bold text-destructive">Unable to Load Orders</h2>
|
|
59
|
+
<p className="text-muted-foreground">{error.message || 'Please try again later.'}</p>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
{!isLoading && !error && orders.length === 0 && (
|
|
64
|
+
<div className="rounded-lg border border-border bg-card p-12 text-center">
|
|
65
|
+
<h3 className="mb-2 text-xl font-semibold">No Orders Yet</h3>
|
|
66
|
+
<p className="mb-4 text-muted-foreground">
|
|
67
|
+
You haven't made any partnership product purchases yet.
|
|
68
|
+
</p>
|
|
69
|
+
<a href="/partnership" className="inline-block rounded-md bg-primary px-6 py-2 text-white hover:bg-primary/90">
|
|
70
|
+
Browse Products
|
|
71
|
+
</a>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{!isLoading && !error && orders.length > 0 && (
|
|
76
|
+
<div className="space-y-4">
|
|
77
|
+
{orders.map((order) => (
|
|
78
|
+
<div
|
|
79
|
+
key={order.id}
|
|
80
|
+
className="cursor-pointer rounded-lg border border-border bg-card p-6 transition-all hover:shadow-lg"
|
|
81
|
+
onClick={() => onOrderClick?.(order.id)}
|
|
82
|
+
>
|
|
83
|
+
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
|
84
|
+
<div className="flex-1">
|
|
85
|
+
<div className="mb-2 flex items-center gap-3">
|
|
86
|
+
<h4 className="text-lg font-semibold">{order.productName}</h4>
|
|
87
|
+
<span className={`text-sm font-medium ${getStatusColor(order.status)}`}>
|
|
88
|
+
{formatStatus(order.status)}
|
|
89
|
+
</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
|
|
92
|
+
<span>Order #{order.id.slice(-8).toUpperCase()}</span>
|
|
93
|
+
<span>
|
|
94
|
+
{formatDistance(new Date(order.created * 1000), new Date(), {
|
|
95
|
+
addSuffix: true,
|
|
96
|
+
})}
|
|
97
|
+
</span>
|
|
98
|
+
{order.quantity && <span>Qty: {order.quantity}</span>}
|
|
99
|
+
{order.selectedVariants && (
|
|
100
|
+
<span>
|
|
101
|
+
{Object.entries(order.selectedVariants)
|
|
102
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
103
|
+
.join(', ')}
|
|
104
|
+
</span>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
{order.trackingNumber && (
|
|
108
|
+
<div className="mt-2 text-sm">
|
|
109
|
+
Tracking: <span className="font-medium">{order.trackingNumber}</span>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
<div className="text-right">
|
|
114
|
+
<p className="text-2xl font-bold">{formatPrice(order.amount, order.currency)}</p>
|
|
115
|
+
<button className="mt-2 text-sm text-primary hover:underline">
|
|
116
|
+
View Details
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
)}
|
|
124
|
+
</div>
|
|
125
|
+
</Screen>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
OrdersHistoryScreen.displayName = 'OrdersHistoryScreen';
|
|
130
|
+
|
|
131
|
+
export default OrdersHistoryScreen;
|
|
132
|
+
export type { OrdersHistoryScreenProps };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface Order {
|
|
2
|
+
id: string;
|
|
3
|
+
status: string;
|
|
4
|
+
amount: number;
|
|
5
|
+
currency: string;
|
|
6
|
+
created: number;
|
|
7
|
+
productId: string;
|
|
8
|
+
productName: string;
|
|
9
|
+
quantity: string;
|
|
10
|
+
partnerId?: string;
|
|
11
|
+
selectedVariants?: Record<string, string>;
|
|
12
|
+
trackingNumber?: string;
|
|
13
|
+
trackingUrl?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface OrdersHistoryScreenProps {
|
|
17
|
+
testID?: string;
|
|
18
|
+
orders: Order[];
|
|
19
|
+
isLoading?: boolean;
|
|
20
|
+
error?: Error | null;
|
|
21
|
+
onOrderClick?: (orderId: string) => void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import PurchaseConfirmationScreen from './PurchaseConfirmationScreen';
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof PurchaseConfirmationScreen> = {
|
|
5
|
+
title: 'Templates/PurchaseConfirmationScreen',
|
|
6
|
+
component: PurchaseConfirmationScreen,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: 'fullscreen',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof PurchaseConfirmationScreen>;
|
|
14
|
+
|
|
15
|
+
export const Default: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
onContinueShopping: () => console.log('Continue Shopping'),
|
|
18
|
+
onGoToDashboard: () => console.log('Go to Dashboard'),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const WithOrderDetails: Story = {
|
|
23
|
+
args: {
|
|
24
|
+
orderDetails: {
|
|
25
|
+
orderId: 'ORD-2024-001234',
|
|
26
|
+
orderDate: 'December 21, 2024',
|
|
27
|
+
items: [
|
|
28
|
+
{
|
|
29
|
+
name: 'NRG Performance Leggings',
|
|
30
|
+
quantity: 1,
|
|
31
|
+
price: 4500,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Athletic Tank Top',
|
|
35
|
+
quantity: 2,
|
|
36
|
+
price: 2500,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
total: 9500,
|
|
40
|
+
currency: 'gbp',
|
|
41
|
+
estimatedDelivery: 'December 28-30, 2024',
|
|
42
|
+
},
|
|
43
|
+
onContinueShopping: () => console.log('Continue Shopping'),
|
|
44
|
+
onViewOrders: () => console.log('View Orders'),
|
|
45
|
+
onGoToDashboard: () => console.log('Go to Dashboard'),
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const CustomMessage: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
title: 'Order Confirmed!',
|
|
52
|
+
message: 'Your order has been successfully placed and is being processed. You will receive shipping updates via email.',
|
|
53
|
+
onContinueShopping: () => console.log('Continue Shopping'),
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const MinimalActions: Story = {
|
|
58
|
+
args: {
|
|
59
|
+
orderDetails: {
|
|
60
|
+
orderId: 'ORD-2024-001234',
|
|
61
|
+
total: 4500,
|
|
62
|
+
currency: 'gbp',
|
|
63
|
+
},
|
|
64
|
+
onContinueShopping: () => console.log('Continue Shopping'),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Screen from '../../components/Screen';
|
|
3
|
+
import { Props } from './PurchaseConfirmationScreen.types';
|
|
4
|
+
import { ShadcnButton } from '../../components/ShadcnButton';
|
|
5
|
+
import { CheckCircle } from 'lucide-react';
|
|
6
|
+
|
|
7
|
+
const PurchaseConfirmationScreen = ({
|
|
8
|
+
testID,
|
|
9
|
+
title = 'Purchase Successful!',
|
|
10
|
+
message = 'Thank you for your purchase. A confirmation email has been sent to your email address with your order details.',
|
|
11
|
+
orderDetails,
|
|
12
|
+
onContinueShopping,
|
|
13
|
+
onViewOrders,
|
|
14
|
+
onGoToDashboard,
|
|
15
|
+
}: Props) => {
|
|
16
|
+
const formatPrice = (price: number, currency: string = 'gbp') => {
|
|
17
|
+
return new Intl.NumberFormat('en-GB', {
|
|
18
|
+
style: 'currency',
|
|
19
|
+
currency: currency.toUpperCase(),
|
|
20
|
+
}).format(price / 100);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Screen data-testid={testID || 'PurchaseConfirmationScreen'}>
|
|
25
|
+
<div className="container mx-auto px-4 py-16">
|
|
26
|
+
<div className="mx-auto max-w-2xl">
|
|
27
|
+
<div className="rounded-lg border border-border bg-card p-8 text-center shadow-lg">
|
|
28
|
+
{/* Success Icon */}
|
|
29
|
+
<div className="mb-6 flex justify-center">
|
|
30
|
+
<CheckCircle className="h-20 w-20 text-green-500" />
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
{/* Title */}
|
|
34
|
+
<h1 className="mb-4 text-3xl font-bold">{title}</h1>
|
|
35
|
+
|
|
36
|
+
{/* Message */}
|
|
37
|
+
<p className="mb-8 text-lg text-muted-foreground">{message}</p>
|
|
38
|
+
|
|
39
|
+
{/* Order Details */}
|
|
40
|
+
{orderDetails && (
|
|
41
|
+
<div className="mb-8 rounded-md bg-muted p-6 text-left">
|
|
42
|
+
<h2 className="mb-4 font-semibold">Order Details</h2>
|
|
43
|
+
|
|
44
|
+
{orderDetails.orderId && (
|
|
45
|
+
<div className="mb-2 flex justify-between text-sm">
|
|
46
|
+
<span className="text-muted-foreground">Order ID:</span>
|
|
47
|
+
<span className="font-medium">{orderDetails.orderId}</span>
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{orderDetails.orderDate && (
|
|
52
|
+
<div className="mb-2 flex justify-between text-sm">
|
|
53
|
+
<span className="text-muted-foreground">Order Date:</span>
|
|
54
|
+
<span className="font-medium">{orderDetails.orderDate}</span>
|
|
55
|
+
</div>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
{orderDetails.items && orderDetails.items.length > 0 && (
|
|
59
|
+
<div className="my-4 border-t border-border pt-4">
|
|
60
|
+
<h3 className="mb-2 text-sm font-semibold">Items:</h3>
|
|
61
|
+
{orderDetails.items.map((item, index) => (
|
|
62
|
+
<div key={index} className="mb-2 flex justify-between text-sm">
|
|
63
|
+
<span>
|
|
64
|
+
{item.name} x {item.quantity}
|
|
65
|
+
</span>
|
|
66
|
+
<span className="font-medium">
|
|
67
|
+
{formatPrice(item.price * item.quantity, orderDetails.currency)}
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{orderDetails.total !== undefined && (
|
|
75
|
+
<div className="mt-4 flex justify-between border-t border-border pt-4 text-base font-bold">
|
|
76
|
+
<span>Total:</span>
|
|
77
|
+
<span>{formatPrice(orderDetails.total, orderDetails.currency)}</span>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{orderDetails.estimatedDelivery && (
|
|
82
|
+
<div className="mt-4 text-sm text-muted-foreground">
|
|
83
|
+
Estimated Delivery: {orderDetails.estimatedDelivery}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
|
|
89
|
+
{/* Information Box */}
|
|
90
|
+
<div className="mb-8 rounded-md bg-muted p-6 text-left">
|
|
91
|
+
<h2 className="mb-3 font-semibold">What happens next?</h2>
|
|
92
|
+
<ul className="space-y-2 text-sm text-muted-foreground">
|
|
93
|
+
<li>✓ You'll receive an order confirmation email shortly</li>
|
|
94
|
+
<li>✓ Your order will be processed and shipped within 3-5 business days</li>
|
|
95
|
+
<li>✓ You'll receive a tracking number once your order ships</li>
|
|
96
|
+
<li>✓ If you have any questions, contact support</li>
|
|
97
|
+
</ul>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{/* Action Buttons */}
|
|
101
|
+
<div className="flex flex-col gap-4 sm:flex-row sm:justify-center">
|
|
102
|
+
{onContinueShopping && (
|
|
103
|
+
<ShadcnButton
|
|
104
|
+
size="lg"
|
|
105
|
+
onClick={onContinueShopping}
|
|
106
|
+
className="w-full sm:w-auto"
|
|
107
|
+
>
|
|
108
|
+
Continue Shopping
|
|
109
|
+
</ShadcnButton>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{onViewOrders && (
|
|
113
|
+
<ShadcnButton
|
|
114
|
+
size="lg"
|
|
115
|
+
variant="outline"
|
|
116
|
+
onClick={onViewOrders}
|
|
117
|
+
className="w-full sm:w-auto"
|
|
118
|
+
>
|
|
119
|
+
View Orders
|
|
120
|
+
</ShadcnButton>
|
|
121
|
+
)}
|
|
122
|
+
|
|
123
|
+
{onGoToDashboard && (
|
|
124
|
+
<ShadcnButton
|
|
125
|
+
size="lg"
|
|
126
|
+
variant="outline"
|
|
127
|
+
onClick={onGoToDashboard}
|
|
128
|
+
className="w-full sm:w-auto"
|
|
129
|
+
>
|
|
130
|
+
Go to Dashboard
|
|
131
|
+
</ShadcnButton>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
</Screen>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
PurchaseConfirmationScreen.displayName = 'PurchaseConfirmationScreen';
|
|
142
|
+
|
|
143
|
+
export default PurchaseConfirmationScreen;
|
|
144
|
+
export type { Props };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface OrderDetails {
|
|
2
|
+
orderId?: string;
|
|
3
|
+
orderDate?: string;
|
|
4
|
+
items?: Array<{
|
|
5
|
+
name: string;
|
|
6
|
+
quantity: number;
|
|
7
|
+
price: number;
|
|
8
|
+
}>;
|
|
9
|
+
total?: number;
|
|
10
|
+
currency?: string;
|
|
11
|
+
shippingAddress?: {
|
|
12
|
+
line1: string;
|
|
13
|
+
line2?: string;
|
|
14
|
+
city: string;
|
|
15
|
+
state: string;
|
|
16
|
+
postal_code: string;
|
|
17
|
+
country: string;
|
|
18
|
+
};
|
|
19
|
+
estimatedDelivery?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface Props {
|
|
23
|
+
testID?: string;
|
|
24
|
+
title?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
orderDetails?: OrderDetails;
|
|
27
|
+
onContinueShopping?: () => void;
|
|
28
|
+
onViewOrders?: () => void;
|
|
29
|
+
onGoToDashboard?: () => void;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './PurchaseConfirmationScreen';
|
package/src/templates/index.ts
CHANGED
|
@@ -11,11 +11,14 @@ export { default as HomeScreen } from './HomeScreen';
|
|
|
11
11
|
export { default as LogoutScreen } from './LogoutScreen';
|
|
12
12
|
export { default as MenuScreen } from './MenuScreen';
|
|
13
13
|
export { default as NotFoundScreen } from './NotFoundScreen';
|
|
14
|
+
export { default as OrderDetailScreen } from './OrderDetailScreen';
|
|
15
|
+
export { default as OrdersHistoryScreen } from './OrdersHistoryScreen';
|
|
14
16
|
export { default as PasswordResetScreen } from './PasswordResetScreen';
|
|
15
17
|
export { default as PasswordResetAuthScreen } from './PasswordResetAuthScreen';
|
|
16
18
|
export { default as ProductDetailScreen } from './ProductDetailScreen';
|
|
17
19
|
export { default as ProductListScreen } from './ProductListScreen';
|
|
18
20
|
export { default as ProfileScreen } from './ProfileScreen';
|
|
21
|
+
export { default as PurchaseConfirmationScreen } from './PurchaseConfirmationScreen';
|
|
19
22
|
export { default as ScheduleScreen } from './ScheduleScreen';
|
|
20
23
|
export { default as SubscriptionScreen } from './SubscriptionScreen';
|
|
21
24
|
export { default as WorkoutScreen } from './WorkoutScreen';
|
|
@@ -34,11 +37,14 @@ export type { Props as HomeScreenProps } from './HomeScreen/HomeScreen.types';
|
|
|
34
37
|
export type { Props as LogoutScreenProps } from './LogoutScreen/LogoutScreen.types';
|
|
35
38
|
export type { Props as MenuScreenProps } from './MenuScreen/MenuScreen.types';
|
|
36
39
|
export type { Props as NotFoundScreenProps } from './NotFoundScreen/NotFoundScreen.types';
|
|
40
|
+
export type { OrderDetailScreenProps, OrderDetail } from './OrderDetailScreen';
|
|
41
|
+
export type { OrdersHistoryScreenProps, Order } from './OrdersHistoryScreen';
|
|
37
42
|
export type { Props as PasswordResetScreenProps } from './PasswordResetScreen/PasswordResetScreen.types';
|
|
38
43
|
export type { Props as PasswordResetAuthScreenProps } from './PasswordResetAuthScreen/PasswordResetAuthScreen.types';
|
|
39
44
|
export type { Props as ProductDetailScreenProps } from './ProductDetailScreen/ProductDetailScreen.types';
|
|
40
45
|
export type { Props as ProductListScreenProps } from './ProductListScreen/ProductListScreen.types';
|
|
41
46
|
export type { Props as ProfileScreenProps } from './ProfileScreen/ProfileScreen.types';
|
|
47
|
+
export type { Props as PurchaseConfirmationScreenProps } from './PurchaseConfirmationScreen/PurchaseConfirmationScreen.types';
|
|
42
48
|
export type { Props as ScheduleScreenProps } from './ScheduleScreen/ScheduleScreen.types';
|
|
43
49
|
export type { Props as SubscriptionScreenProps } from './SubscriptionScreen/SubscriptionScreen.types';
|
|
44
50
|
export type { Props as WorkoutScreenProps } from './WorkoutScreen/WorkoutScreen.types';
|