@carlonicora/nextjs-jsonapi 1.28.0 → 1.29.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.
Files changed (95) hide show
  1. package/dist/{BlockNoteEditor-CAUNVZUF.js → BlockNoteEditor-YBVEOPV4.js} +13 -13
  2. package/dist/{BlockNoteEditor-CAUNVZUF.js.map → BlockNoteEditor-YBVEOPV4.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-EOA4OEVX.mjs → BlockNoteEditor-ZM4YPXHO.mjs} +3 -3
  4. package/dist/billing/index.d.mts +47 -17
  5. package/dist/billing/index.d.ts +47 -17
  6. package/dist/billing/index.js +1241 -1073
  7. package/dist/billing/index.js.map +1 -1
  8. package/dist/billing/index.mjs +1375 -1207
  9. package/dist/billing/index.mjs.map +1 -1
  10. package/dist/{chunk-IXI4GAKB.js → chunk-3X7EEFMN.js} +488 -431
  11. package/dist/chunk-3X7EEFMN.js.map +1 -0
  12. package/dist/{chunk-ORFXBO7F.mjs → chunk-DU64WMZD.mjs} +6 -3
  13. package/dist/chunk-DU64WMZD.mjs.map +1 -0
  14. package/dist/{chunk-TSEU4KZ2.js → chunk-J22NEVSK.js} +21 -18
  15. package/dist/chunk-J22NEVSK.js.map +1 -0
  16. package/dist/{chunk-PYASRX75.mjs → chunk-UCD5CUE4.mjs} +81 -24
  17. package/dist/chunk-UCD5CUE4.mjs.map +1 -0
  18. package/dist/client/index.d.mts +14 -5
  19. package/dist/client/index.d.ts +14 -5
  20. package/dist/client/index.js +5 -3
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/client/index.mjs +4 -2
  23. package/dist/components/index.d.mts +2 -2
  24. package/dist/components/index.d.ts +2 -2
  25. package/dist/components/index.js +3 -3
  26. package/dist/components/index.mjs +2 -2
  27. package/dist/{config-B4pZpLT9.d.ts → config-CHwoRDOp.d.ts} +1 -1
  28. package/dist/{config-DT1K-t6I.d.mts → config-DiWyJzk9.d.mts} +1 -1
  29. package/dist/{content.interface-B2Ldg0vg.d.mts → content.interface-BSpowEiW.d.mts} +1 -1
  30. package/dist/{content.interface-D8NHv3DX.d.ts → content.interface-DFQ7mkpL.d.ts} +1 -1
  31. package/dist/contexts/index.d.mts +2 -2
  32. package/dist/contexts/index.d.ts +2 -2
  33. package/dist/contexts/index.js +3 -3
  34. package/dist/contexts/index.mjs +2 -2
  35. package/dist/core/index.d.mts +39 -37
  36. package/dist/core/index.d.ts +39 -37
  37. package/dist/core/index.js +2 -2
  38. package/dist/core/index.mjs +1 -1
  39. package/dist/index.d.mts +4 -4
  40. package/dist/index.d.ts +4 -4
  41. package/dist/index.js +2 -2
  42. package/dist/index.mjs +1 -1
  43. package/dist/{notification.interface-H0L9WBge.d.ts → notification.interface-CmKmObIU.d.ts} +1 -0
  44. package/dist/{notification.interface-DEn-Yp_b.d.mts → notification.interface-D5MbtfZK.d.mts} +1 -0
  45. package/dist/{s3.service-BNytYanU.d.mts → s3.service-BMT7W6KS.d.mts} +19 -19
  46. package/dist/{s3.service-C7f_Ygz5.d.ts → s3.service-DsXo9nop.d.ts} +19 -19
  47. package/dist/server/index.d.mts +3 -3
  48. package/dist/server/index.d.ts +3 -3
  49. package/dist/server/index.js +3 -3
  50. package/dist/server/index.mjs +1 -1
  51. package/dist/{useSocket-BcnThTD0.d.mts → useSocket-DUqGoPya.d.mts} +1 -1
  52. package/dist/{useSocket-QZTOCzRF.d.ts → useSocket-QuHa0ZmO.d.ts} +1 -1
  53. package/package.json +1 -1
  54. package/src/client/index.ts +1 -0
  55. package/src/components/forms/FormSelect.tsx +2 -1
  56. package/src/features/auth/data/auth.ts +0 -2
  57. package/src/features/billing/components/containers/BillingDashboardContainer.tsx +60 -3
  58. package/src/features/billing/stripe-customer/components/forms/PaymentMethodEditor.tsx +12 -152
  59. package/src/features/billing/stripe-customer/components/forms/PaymentMethodForm.tsx +168 -0
  60. package/src/features/billing/stripe-customer/components/forms/index.ts +1 -0
  61. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +19 -1
  62. package/src/features/billing/stripe-product/components/forms/ProductEditor.tsx +2 -2
  63. package/src/features/billing/stripe-subscription/components/containers/SubscriptionsContainer.tsx +24 -235
  64. package/src/features/billing/stripe-subscription/components/details/SubscriptionDetails.tsx +7 -18
  65. package/src/features/billing/stripe-subscription/components/forms/index.ts +0 -1
  66. package/src/features/billing/stripe-subscription/components/lists/SubscriptionsList.tsx +10 -1
  67. package/src/features/billing/stripe-subscription/components/widgets/IntervalToggle.tsx +28 -0
  68. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingList.tsx +128 -0
  69. package/src/features/billing/stripe-subscription/components/widgets/ProductPricingRow.tsx +54 -0
  70. package/src/features/billing/stripe-subscription/components/widgets/SubscriptionConfirmation.tsx +68 -0
  71. package/src/features/billing/stripe-subscription/components/widgets/index.ts +4 -1
  72. package/src/features/billing/stripe-subscription/components/wizards/SubscriptionWizard.tsx +114 -0
  73. package/src/features/billing/stripe-subscription/components/wizards/WizardProgressIndicator.tsx +66 -0
  74. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPaymentMethod.tsx +32 -0
  75. package/src/features/billing/stripe-subscription/components/wizards/WizardStepPlanSelection.tsx +103 -0
  76. package/src/features/billing/stripe-subscription/components/wizards/WizardStepReview.tsx +133 -0
  77. package/src/features/billing/stripe-subscription/components/wizards/index.ts +6 -0
  78. package/src/features/billing/stripe-subscription/hooks/useSubscriptionWizard.ts +217 -0
  79. package/src/features/billing/stripe-subscription/index.ts +3 -2
  80. package/src/features/company/components/details/TokenStatusIndicator.tsx +19 -9
  81. package/src/features/company/data/company.interface.ts +2 -0
  82. package/src/features/company/data/company.ts +7 -0
  83. package/src/features/company/hooks/index.ts +1 -0
  84. package/src/features/company/hooks/useSubscriptionStatus.ts +71 -0
  85. package/src/features/user/components/forms/UserEditor.tsx +1 -1
  86. package/src/features/user/components/lists/AdminUsersList.tsx +1 -1
  87. package/src/features/user/contexts/CurrentUserContext.tsx +1 -1
  88. package/src/features/user/data/user.ts +1 -1
  89. package/dist/chunk-IXI4GAKB.js.map +0 -1
  90. package/dist/chunk-ORFXBO7F.mjs.map +0 -1
  91. package/dist/chunk-PYASRX75.mjs.map +0 -1
  92. package/dist/chunk-TSEU4KZ2.js.map +0 -1
  93. package/src/features/billing/stripe-subscription/components/forms/SubscriptionEditor.tsx +0 -331
  94. package/src/features/billing/stripe-subscription/components/widgets/PricingCardsGrid.tsx +0 -110
  95. /package/dist/{BlockNoteEditor-EOA4OEVX.mjs.map → BlockNoteEditor-ZM4YPXHO.mjs.map} +0 -0
@@ -1,331 +0,0 @@
1
- "use client";
2
-
3
- import { CheckCircle, Loader2 } from "lucide-react";
4
- import { useEffect, useState } from "react";
5
- import { v4 } from "uuid";
6
- import {
7
- Alert,
8
- AlertDescription,
9
- AlertTitle,
10
- Button,
11
- Dialog,
12
- DialogContent,
13
- DialogDescription,
14
- DialogHeader,
15
- DialogTitle,
16
- } from "../../../../../shadcnui";
17
- import { StripeCustomerService } from "../../../stripe-customer/data/stripe-customer.service";
18
- import { ProrationPreviewInterface } from "../../../stripe-invoice/data/stripe-invoice.interface";
19
- import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
20
- import { StripeProductInterface, StripeProductService } from "../../../stripe-product";
21
- import { StripeSubscriptionInterface, StripeSubscriptionService } from "../../data";
22
- import { useConfirmSubscriptionPayment } from "../../hooks";
23
- import { PricesByProduct, PricingCardsGrid } from "../widgets/PricingCardsGrid";
24
- import { ProrationPreview } from "../widgets/ProrationPreview";
25
-
26
- type PaymentConfirmationState = "idle" | "confirming" | "success" | "error";
27
-
28
- type SubscriptionEditorProps = {
29
- subscription?: StripeSubscriptionInterface;
30
- open: boolean;
31
- onOpenChange: (open: boolean) => void;
32
- onSuccess: () => void;
33
- onAddPaymentMethod?: () => void;
34
- };
35
-
36
- export function SubscriptionEditor({
37
- subscription,
38
- open,
39
- onOpenChange,
40
- onSuccess,
41
- onAddPaymentMethod,
42
- }: SubscriptionEditorProps) {
43
- const { confirmPayment, isConfirming } = useConfirmSubscriptionPayment();
44
-
45
- const [products, setProducts] = useState<StripeProductInterface[]>([]);
46
- const [pricesByProduct, setPricesByProduct] = useState<PricesByProduct>(new Map());
47
- const [loading, setLoading] = useState<boolean>(true);
48
- const [selectedPriceId, setSelectedPriceId] = useState<string | null>(null);
49
- const [loadingPriceId, setLoadingPriceId] = useState<string | null>(null);
50
- const [prorationPreview, setProrationPreview] = useState<ProrationPreviewInterface | null>(null);
51
- const [loadingProration, setLoadingProration] = useState<boolean>(false);
52
- const [hasPaymentMethod, setHasPaymentMethod] = useState<boolean>(true);
53
- const [loadingPaymentMethods, setLoadingPaymentMethods] = useState<boolean>(true);
54
- const [paymentRequiredError, setPaymentRequiredError] = useState<boolean>(false);
55
- const [paymentConfirmationState, setPaymentConfirmationState] = useState<PaymentConfirmationState>("idle");
56
- const [paymentError, setPaymentError] = useState<string | null>(null);
57
-
58
- // Get current subscription price if editing (use internal UUID for comparison)
59
- const currentPriceId = subscription?.price?.id;
60
- const isEditMode = !!subscription;
61
-
62
- // Check payment methods on mount (only for new subscriptions)
63
- useEffect(() => {
64
- const checkPaymentMethods = async () => {
65
- if (subscription) {
66
- // Editing existing subscription doesn't need payment method check
67
- setLoadingPaymentMethods(false);
68
- return;
69
- }
70
-
71
- setLoadingPaymentMethods(true);
72
- try {
73
- const paymentMethods = await StripeCustomerService.listPaymentMethods();
74
- const hasMethod = paymentMethods.length > 0;
75
- setHasPaymentMethod(hasMethod);
76
- } catch (error) {
77
- console.error("[SubscriptionEditor] Failed to check payment methods:", error);
78
- setHasPaymentMethod(false);
79
- } finally {
80
- setLoadingPaymentMethods(false);
81
- }
82
- };
83
-
84
- if (open) {
85
- checkPaymentMethods();
86
- }
87
- }, [open, subscription]);
88
-
89
- // Load products with prices on mount
90
- useEffect(() => {
91
- const loadData = async () => {
92
- setLoading(true);
93
- try {
94
- const fetchedProducts = await StripeProductService.listProducts({ active: true });
95
-
96
- // Build prices map from product.stripePrices
97
- const grouped: PricesByProduct = new Map();
98
- for (const product of fetchedProducts) {
99
- if (product.stripePrices && product.stripePrices.length > 0) {
100
- grouped.set(product.id, product.stripePrices);
101
- }
102
- }
103
-
104
- setProducts(fetchedProducts);
105
- setPricesByProduct(grouped);
106
- } catch (error) {
107
- console.error("[SubscriptionEditor] Failed to load products/prices:", error);
108
- } finally {
109
- setLoading(false);
110
- }
111
- };
112
-
113
- if (open) {
114
- loadData();
115
- }
116
- }, [open]);
117
-
118
- // Load proration preview when editing and price is selected
119
- useEffect(() => {
120
- const loadProration = async () => {
121
- if (!subscription || !selectedPriceId || selectedPriceId === currentPriceId) {
122
- setProrationPreview(null);
123
- return;
124
- }
125
-
126
- setLoadingProration(true);
127
- try {
128
- const preview = await StripeSubscriptionService.getProrationPreview({
129
- subscriptionId: subscription.id,
130
- newPriceId: selectedPriceId,
131
- });
132
- setProrationPreview(preview);
133
- } catch (error) {
134
- console.error("[SubscriptionEditor] Failed to load proration preview:", error);
135
- setProrationPreview(null);
136
- } finally {
137
- setLoadingProration(false);
138
- }
139
- };
140
-
141
- loadProration();
142
- }, [selectedPriceId, subscription, currentPriceId]);
143
-
144
- const handleSelectPrice = async (price: StripePriceInterface) => {
145
- const priceId = price.id; // Use internal UUID, not Stripe ID
146
-
147
- if (isEditMode) {
148
- // Edit mode: just select the price to show proration preview
149
- setSelectedPriceId(priceId);
150
- } else {
151
- // Create mode: immediately create subscription
152
- setLoadingPriceId(priceId);
153
- setSelectedPriceId(priceId);
154
- setPaymentError(null);
155
- setPaymentConfirmationState("idle");
156
-
157
- try {
158
- const result = await StripeSubscriptionService.createSubscription({
159
- id: v4(),
160
- priceId,
161
- });
162
-
163
- // Check if payment confirmation is required (SCA flow)
164
- if (result.meta.requiresAction && result.meta.clientSecret) {
165
- setPaymentConfirmationState("confirming");
166
-
167
- const confirmation = await confirmPayment(result.meta.clientSecret);
168
-
169
- if (!confirmation.success) {
170
- console.error("[SubscriptionEditor] Payment confirmation failed:", confirmation.error);
171
- setPaymentConfirmationState("error");
172
- setPaymentError(confirmation.error || "Payment confirmation failed");
173
- setLoadingPriceId(null);
174
- return;
175
- }
176
-
177
- // Sync subscription to get updated status from Stripe
178
- await StripeSubscriptionService.syncSubscription({
179
- subscriptionId: result.subscription.id,
180
- });
181
- }
182
-
183
- // Success - show brief success state then close
184
- setPaymentConfirmationState("success");
185
- setTimeout(() => {
186
- onSuccess();
187
- onOpenChange(false);
188
- }, 1000);
189
- } catch (error: any) {
190
- console.error("[SubscriptionEditor] Failed to create subscription:", error);
191
- // Handle 402 Payment Required error
192
- if (error?.status === 402 || error?.response?.status === 402) {
193
- setPaymentRequiredError(true);
194
- setHasPaymentMethod(false);
195
- } else {
196
- setPaymentConfirmationState("error");
197
- setPaymentError(error?.message || "Failed to create subscription");
198
- }
199
- setLoadingPriceId(null);
200
- }
201
- }
202
- };
203
-
204
- const handleConfirmPlanChange = async () => {
205
- if (!subscription || !selectedPriceId) return;
206
-
207
- setLoadingPriceId(selectedPriceId);
208
-
209
- try {
210
- await StripeSubscriptionService.changePlan({
211
- id: subscription.id,
212
- newPriceId: selectedPriceId,
213
- });
214
- onSuccess();
215
- onOpenChange(false);
216
- } catch (error) {
217
- console.error("[SubscriptionEditor] Failed to change plan:", error);
218
- } finally {
219
- setLoadingPriceId(null);
220
- }
221
- };
222
-
223
- const handleCancel = () => {
224
- setSelectedPriceId(null);
225
- setProrationPreview(null);
226
- };
227
-
228
- return (
229
- <Dialog open={open} onOpenChange={onOpenChange}>
230
- <DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
231
- <DialogHeader>
232
- <DialogTitle>{subscription ? "Change Plan" : "Subscribe to a Plan"}</DialogTitle>
233
- <DialogDescription>
234
- {subscription
235
- ? "Select a new plan to switch to. You'll see a proration preview before confirming."
236
- : "Choose a plan to start your subscription."}
237
- </DialogDescription>
238
- </DialogHeader>
239
-
240
- {loadingPaymentMethods && !subscription ? (
241
- <div className="flex items-center justify-center py-8">
242
- <div className="text-muted-foreground">Checking payment methods...</div>
243
- </div>
244
- ) : !hasPaymentMethod && !subscription ? (
245
- <Alert variant="destructive">
246
- <AlertTitle>Payment Method Required</AlertTitle>
247
- <AlertDescription className="mt-2">
248
- <p className="mb-4">
249
- {paymentRequiredError
250
- ? "Your subscription could not be created because no payment method is on file."
251
- : "You need to add a payment method before you can subscribe to a plan."}
252
- </p>
253
- {onAddPaymentMethod && (
254
- <Button onClick={onAddPaymentMethod} variant="outline">
255
- Add Payment Method
256
- </Button>
257
- )}
258
- </AlertDescription>
259
- </Alert>
260
- ) : paymentConfirmationState === "confirming" || isConfirming ? (
261
- <div className="flex flex-col items-center justify-center py-12 space-y-4">
262
- <Loader2 className="h-8 w-8 animate-spin text-primary" />
263
- <div className="text-center">
264
- <p className="font-medium">Processing payment...</p>
265
- <p className="text-sm text-muted-foreground">Please complete any verification if prompted.</p>
266
- </div>
267
- </div>
268
- ) : paymentConfirmationState === "success" ? (
269
- <div className="flex flex-col items-center justify-center py-12 space-y-4">
270
- <CheckCircle className="h-12 w-12 text-green-500" />
271
- <div className="text-center">
272
- <p className="font-medium text-green-600">Payment successful!</p>
273
- <p className="text-sm text-muted-foreground">Your subscription is now active.</p>
274
- </div>
275
- </div>
276
- ) : paymentConfirmationState === "error" ? (
277
- <div className="space-y-4">
278
- <Alert variant="destructive">
279
- <AlertTitle>Payment Failed</AlertTitle>
280
- <AlertDescription className="mt-2">
281
- <p className="mb-4">{paymentError || "We couldn't process your payment. Please try again."}</p>
282
- <Button
283
- onClick={() => {
284
- setPaymentConfirmationState("idle");
285
- setPaymentError(null);
286
- setLoadingPriceId(null);
287
- }}
288
- variant="outline"
289
- >
290
- Try Again
291
- </Button>
292
- </AlertDescription>
293
- </Alert>
294
- </div>
295
- ) : (
296
- <div className="space-y-6">
297
- <PricingCardsGrid
298
- products={products}
299
- pricesByProduct={pricesByProduct}
300
- currentPriceId={currentPriceId}
301
- selectedPriceId={selectedPriceId ?? undefined}
302
- loadingPriceId={loadingPriceId ?? undefined}
303
- loading={loading}
304
- onSelectPrice={handleSelectPrice}
305
- />
306
-
307
- {isEditMode && loadingProration && (
308
- <div className="bg-muted/50 rounded-lg p-4 text-sm text-muted-foreground text-center">
309
- Loading proration preview...
310
- </div>
311
- )}
312
-
313
- {isEditMode && prorationPreview && !loadingProration && (
314
- <div className="space-y-4">
315
- <ProrationPreview preview={prorationPreview} />
316
- <div className="flex justify-end gap-3">
317
- <Button variant="outline" onClick={handleCancel} disabled={!!loadingPriceId}>
318
- Cancel
319
- </Button>
320
- <Button onClick={handleConfirmPlanChange} disabled={!!loadingPriceId}>
321
- {loadingPriceId ? "Processing..." : "Confirm Plan Change"}
322
- </Button>
323
- </div>
324
- </div>
325
- )}
326
- </div>
327
- )}
328
- </DialogContent>
329
- </Dialog>
330
- );
331
- }
@@ -1,110 +0,0 @@
1
- "use client";
2
-
3
- import { Card, CardContent, CardFooter, CardHeader, Skeleton } from "../../../../../shadcnui";
4
- import { StripePriceInterface } from "../../../stripe-price/data/stripe-price.interface";
5
- import { StripeProductInterface } from "../../../stripe-product";
6
- import { PricingCard } from "./PricingCard";
7
-
8
- export type PricesByProduct = Map<string, StripePriceInterface[]>;
9
-
10
- export type PricingCardsGridProps = {
11
- products: StripeProductInterface[];
12
- pricesByProduct: PricesByProduct;
13
- currentPriceId?: string;
14
- selectedPriceId?: string;
15
- loadingPriceId?: string;
16
- loading?: boolean;
17
- onSelectPrice: (price: StripePriceInterface) => void;
18
- };
19
-
20
- export function PricingCardsGrid({
21
- products,
22
- pricesByProduct,
23
- currentPriceId,
24
- selectedPriceId,
25
- loadingPriceId,
26
- loading = false,
27
- onSelectPrice,
28
- }: PricingCardsGridProps) {
29
- if (loading) {
30
- return <PricingCardsGridSkeleton />;
31
- }
32
-
33
- if (products.length === 0) {
34
- return (
35
- <div className="text-center py-8 text-muted-foreground">
36
- No plans available
37
- </div>
38
- );
39
- }
40
-
41
- return (
42
- <div className="space-y-8" role="radiogroup" aria-label="Available pricing plans">
43
- {products.map((product) => {
44
- const prices = pricesByProduct.get(product.id) || [];
45
- if (prices.length === 0) return null;
46
-
47
- // Sort prices from cheapest to most expensive
48
- const sortedPrices = [...prices].sort((a, b) => (a.unitAmount ?? 0) - (b.unitAmount ?? 0));
49
-
50
- return (
51
- <div key={product.id} className="space-y-4">
52
- <h3 className="text-lg font-semibold">{product.name}</h3>
53
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
54
- {sortedPrices.map((price) => (
55
- <PricingCard
56
- key={price.stripePriceId}
57
- price={price}
58
- isCurrentPlan={price.stripePriceId === currentPriceId}
59
- isSelected={price.stripePriceId === selectedPriceId}
60
- isLoading={price.stripePriceId === loadingPriceId}
61
- onSelect={onSelectPrice}
62
- />
63
- ))}
64
- </div>
65
- </div>
66
- );
67
- })}
68
- </div>
69
- );
70
- }
71
-
72
- function PricingCardsGridSkeleton() {
73
- return (
74
- <div className="space-y-8">
75
- {[1, 2].map((productIndex) => (
76
- <div key={productIndex} className="space-y-4">
77
- <Skeleton className="h-6 w-32" />
78
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
79
- {[1, 2, 3].map((cardIndex) => (
80
- <Card key={cardIndex} className="animate-pulse">
81
- <CardHeader className="pb-2">
82
- <Skeleton className="h-5 w-24" />
83
- </CardHeader>
84
- <CardContent className="pb-4">
85
- <div className="mb-4">
86
- <Skeleton className="h-9 w-20 inline-block" />
87
- <Skeleton className="h-4 w-12 inline-block ml-2" />
88
- </div>
89
- <div className="space-y-2">
90
- <div className="flex items-center gap-2">
91
- <Skeleton className="h-4 w-4 rounded-full" />
92
- <Skeleton className="h-4 w-32" />
93
- </div>
94
- <div className="flex items-center gap-2">
95
- <Skeleton className="h-4 w-4 rounded-full" />
96
- <Skeleton className="h-4 w-28" />
97
- </div>
98
- </div>
99
- </CardContent>
100
- <CardFooter>
101
- <Skeleton className="h-9 w-full" />
102
- </CardFooter>
103
- </Card>
104
- ))}
105
- </div>
106
- </div>
107
- ))}
108
- </div>
109
- );
110
- }