@salifbiaye/create-boutique 0.1.4 → 0.2.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 (72) hide show
  1. package/cli/dist/commands.js +3 -3
  2. package/cli/dist/create.js +5 -5
  3. package/cli/dist/generate.js +68 -10
  4. package/cli/dist/project.js +2 -0
  5. package/cli/dist/themes.js +5 -1
  6. package/docs/guide-complet.en.html +11 -0
  7. package/docs/guide-complet.fr.html +11 -0
  8. package/package.json +1 -1
  9. package/templates/backend/src/lib/emails.ts +8 -1
  10. package/templates/backend/src/scripts/campaign-import.ts +6 -4
  11. package/templates/backend/src/scripts/seed.ts +15 -3
  12. package/templates/storefront/locales/en.json +40 -3
  13. package/templates/storefront/locales/fr.json +40 -3
  14. package/templates/storefront/package-lock.json +7 -0
  15. package/templates/storefront/package.json +1 -0
  16. package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +3 -9
  17. package/templates/storefront/src/app/(main)/page.tsx +25 -0
  18. package/templates/storefront/src/app/(main)/store/page.tsx +3 -1
  19. package/templates/storefront/src/app/layout.tsx +9 -2
  20. package/templates/storefront/src/lib/boutique/config.ts +1 -0
  21. package/templates/storefront/src/lib/boutique/i18n-client.tsx +10 -2
  22. package/templates/storefront/src/lib/boutique/schema.ts +2 -0
  23. package/templates/storefront/src/lib/constants.tsx +5 -4
  24. package/templates/storefront/src/modules/account/components/account-nav/index.tsx +50 -177
  25. package/templates/storefront/src/modules/account/components/address-card/add-address.tsx +2 -2
  26. package/templates/storefront/src/modules/account/components/address-card/edit-address-modal.tsx +2 -2
  27. package/templates/storefront/src/modules/account/components/overview/index.tsx +98 -143
  28. package/templates/storefront/src/modules/account/components/profile-billing-address/index.tsx +2 -3
  29. package/templates/storefront/src/modules/account/components/profile-phone/index.tsx +2 -3
  30. package/templates/storefront/src/modules/account/components/register/index.tsx +2 -3
  31. package/templates/storefront/src/modules/account/templates/account-layout.tsx +20 -24
  32. package/templates/storefront/src/modules/checkout/components/billing_address/index.tsx +2 -2
  33. package/templates/storefront/src/modules/checkout/components/payment/index.tsx +2 -1
  34. package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +65 -48
  35. package/templates/storefront/src/modules/checkout/components/payment-container/index.tsx +23 -29
  36. package/templates/storefront/src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx +51 -6
  37. package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +2 -2
  38. package/templates/storefront/src/modules/common/components/phone-input/index.tsx +110 -0
  39. package/templates/storefront/src/modules/store/templates/index.tsx +82 -17
  40. package/templates/storefront/src/modules/store/templates/paginated-products.tsx +5 -0
  41. package/templates/storefront/src/modules/ub/campaign-blocks.tsx +1 -1
  42. package/templates/storefront/src/modules/ub/contact-page.tsx +78 -0
  43. package/templates/storefront/src/modules/ub/footer.tsx +26 -5
  44. package/templates/storefront/src/modules/ub/mobile-menu.tsx +49 -0
  45. package/templates/storefront/src/modules/ub/nav.tsx +2 -0
  46. package/templates/storefront/src/modules/ub/showcase.tsx +137 -0
  47. package/templates/storefront/src/proxy.ts +3 -3
  48. package/templates/storefront/src/styles/ub/account.css +199 -0
  49. package/templates/storefront/src/styles/ub/black-friday.css +96 -0
  50. package/templates/storefront/src/styles/ub/forms.css +61 -0
  51. package/templates/storefront/src/styles/ub/pages.css +382 -0
  52. package/templates/storefront/src/styles/ub/responsive.css +57 -0
  53. package/templates/storefront/src/styles/ub/showcase.css +85 -0
  54. package/templates/storefront/src/styles/ub/variants.css +64 -0
  55. package/templates/storefront/themes/black-friday-2/campaign.en.json +31 -0
  56. package/templates/storefront/themes/black-friday-2/campaign.fr.json +31 -0
  57. package/templates/storefront/themes/black-friday-2/theme.json +10 -0
  58. package/templates/storefront/themes/cyber-monday-2/campaign.en.json +31 -0
  59. package/templates/storefront/themes/cyber-monday-2/campaign.fr.json +31 -0
  60. package/templates/storefront/themes/cyber-monday-2/theme.json +10 -0
  61. package/templates/storefront/themes/halloween-2/campaign.en.json +31 -0
  62. package/templates/storefront/themes/halloween-2/campaign.fr.json +31 -0
  63. package/templates/storefront/themes/halloween-2/theme.json +10 -0
  64. package/templates/storefront/themes/noel-2/campaign.en.json +31 -0
  65. package/templates/storefront/themes/noel-2/campaign.fr.json +31 -0
  66. package/templates/storefront/themes/noel-2/theme.json +10 -0
  67. package/templates/storefront/themes/nouvel-an-2/campaign.en.json +31 -0
  68. package/templates/storefront/themes/nouvel-an-2/campaign.fr.json +31 -0
  69. package/templates/storefront/themes/nouvel-an-2/theme.json +10 -0
  70. package/templates/storefront/themes/thanksgiving-2/campaign.en.json +31 -0
  71. package/templates/storefront/themes/thanksgiving-2/campaign.fr.json +31 -0
  72. package/templates/storefront/themes/thanksgiving-2/theme.json +10 -0
@@ -1,168 +1,123 @@
1
- import { T } from "@lib/boutique/i18n-client"
2
- import { Container } from "@medusajs/ui"
1
+ import Link from "next/link"
3
2
 
4
- import ChevronDown from "@modules/common/icons/chevron-down"
5
- import LocalizedClientLink from "@modules/common/components/localized-client-link"
3
+ import { getBoutique } from "@lib/boutique/config"
4
+ import { t } from "@lib/boutique/i18n"
6
5
  import { convertToLocale } from "@lib/util/money"
7
6
  import { HttpTypes } from "@medusajs/types"
7
+ import { Icon } from "@modules/ub/icons"
8
8
 
9
9
  type OverviewProps = {
10
10
  customer: HttpTypes.StoreCustomer | null
11
11
  orders: HttpTypes.StoreOrder[] | null
12
12
  }
13
13
 
14
+ /** Tableau de bord du compte : carte d'accueil, 3 indicateurs cliquables, commandes récentes. Composant serveur. */
14
15
  const Overview = ({ customer, orders }: OverviewProps) => {
16
+ const { locale } = getBoutique()
17
+ const date = new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric" })
18
+ const completion = getProfileCompletion(customer)
19
+ const stats = [
20
+ { href: "/account/orders", icon: "cart" as const, value: String(orders?.length ?? 0), label: t("account.orders"), testId: "orders-count" },
21
+ { href: "/account/profile", icon: "user" as const, value: `${completion}%`, label: t("account.profileDone"), testId: "customer-profile-completion", data: completion },
22
+ { href: "/account/addresses", icon: "home" as const, value: String(customer?.addresses?.length || 0), label: t("account.addresses"), testId: "addresses-count", data: customer?.addresses?.length || 0 },
23
+ ]
24
+
15
25
  return (
16
- <div data-testid="overview-page-wrapper">
17
- <div className="hidden small:block">
18
- <div className="text-xl-semi flex justify-between items-center mb-4">
19
- <span data-testid="welcome-message" data-value={customer?.first_name}>
20
- <T k="account.hello" vars={{ name: customer?.first_name ?? "" }} />
21
- </span>
22
- <span className="text-small-regular text-ui-fg-base">
23
- <T k="account.signedInAs" />{" "}
24
- <span
25
- className="font-semibold"
26
- data-testid="customer-email"
27
- data-value={customer?.email}
28
- >
26
+ <div className="ub-acc" data-testid="overview-page-wrapper">
27
+ <section className="ub-acc__hello">
28
+ <div>
29
+ <h1 className="ub-head" data-testid="welcome-message" data-value={customer?.first_name}>
30
+ {t("account.hello", { name: customer?.first_name ?? "" })}
31
+ </h1>
32
+ <p>
33
+ {t("account.signedInAs")}{" "}
34
+ <b data-testid="customer-email" data-value={customer?.email}>
29
35
  {customer?.email}
30
- </span>
31
- </span>
36
+ </b>
37
+ </p>
32
38
  </div>
33
- <div className="flex flex-col py-8 border-t border-gray-200">
34
- <div className="flex flex-col gap-y-4 h-full col-span-1 row-span-2 flex-1">
35
- <div className="flex items-start gap-x-16 mb-6">
36
- <div className="flex flex-col gap-y-4">
37
- <h3 className="text-large-semi"><T k="account.profile" /></h3>
38
- <div className="flex items-end gap-x-2">
39
- <span
40
- className="text-3xl-semi leading-none"
41
- data-testid="customer-profile-completion"
42
- data-value={getProfileCompletion(customer)}
43
- >
44
- {getProfileCompletion(customer)}%
45
- </span>
46
- <span className="uppercase text-base-regular text-ui-fg-subtle">
47
- <T k="account.completed" />
48
- </span>
49
- </div>
50
- </div>
39
+ <Link href="/store" className="ub-acc__shop">
40
+ {t("account.keepShopping")} <Icon name="arrowRight" />
41
+ </Link>
42
+ </section>
51
43
 
52
- <div className="flex flex-col gap-y-4">
53
- <h3 className="text-large-semi"><T k="account.addresses" /></h3>
54
- <div className="flex items-end gap-x-2">
55
- <span
56
- className="text-3xl-semi leading-none"
57
- data-testid="addresses-count"
58
- data-value={customer?.addresses?.length || 0}
59
- >
60
- {customer?.addresses?.length || 0}
61
- </span>
62
- <span className="uppercase text-base-regular text-ui-fg-subtle">
63
- <T k="account.saved" />
64
- </span>
65
- </div>
66
- </div>
67
- </div>
44
+ <ul className="ub-acc__stats">
45
+ {stats.map((s) => (
46
+ <li key={s.href}>
47
+ <Link href={s.href} className="ub-acc__stat">
48
+ <span className="ub-acc__ic">
49
+ <Icon name={s.icon} />
50
+ </span>
51
+ <b data-testid={s.testId} data-value={s.data ?? s.value}>
52
+ {s.value}
53
+ </b>
54
+ <small>{s.label}</small>
55
+ </Link>
56
+ </li>
57
+ ))}
58
+ </ul>
68
59
 
69
- <div className="flex flex-col gap-y-4">
70
- <div className="flex items-center gap-x-2">
71
- <h3 className="text-large-semi"><T k="account.recentOrders" /></h3>
72
- </div>
73
- <ul
74
- className="flex flex-col gap-y-4"
75
- data-testid="orders-wrapper"
76
- >
77
- {orders && orders.length > 0 ? (
78
- orders.slice(0, 5).map((order) => {
79
- return (
80
- <li
81
- key={order.id}
82
- data-testid="order-wrapper"
83
- data-value={order.id}
84
- >
85
- <LocalizedClientLink
86
- href={`/account/orders/details/${order.id}`}
87
- >
88
- <Container className="bg-gray-50 flex justify-between items-center p-4">
89
- <div className="grid grid-cols-3 grid-rows-2 text-small-regular gap-x-4 flex-1">
90
- <span className="font-semibold">Date placed</span>
91
- <span className="font-semibold">
92
- <T k="account.orderNumber" />
93
- </span>
94
- <span className="font-semibold">
95
- <T k="account.totalAmount" />
96
- </span>
97
- <span data-testid="order-created-date">
98
- {new Date(order.created_at).toDateString()}
99
- </span>
100
- <span
101
- data-testid="order-id"
102
- data-value={order.display_id}
103
- >
104
- #{order.display_id}
105
- </span>
106
- <span data-testid="order-amount">
107
- {convertToLocale({
108
- amount: order.total,
109
- currency_code: order.currency_code,
110
- })}
111
- </span>
112
- </div>
113
- <button
114
- className="flex items-center justify-between"
115
- data-testid="open-order-button"
116
- >
117
- <span className="sr-only">
118
- Go to order #{order.display_id}
119
- </span>
120
- <ChevronDown className="-rotate-90" />
121
- </button>
122
- </Container>
123
- </LocalizedClientLink>
124
- </li>
125
- )
126
- })
127
- ) : (
128
- <span data-testid="no-orders-message"><T k="account.noRecentOrders" /></span>
129
- )}
130
- </ul>
131
- </div>
132
- </div>
60
+ {completion < 100 && (
61
+ <div className="ub-acc__progress">
62
+ <span>{t("account.completeProfile")}</span>
63
+ <i style={{ ["--p" as string]: `${completion}%` }} />
133
64
  </div>
134
- </div>
65
+ )}
66
+
67
+ <section>
68
+ <div className="ub-acc__head">
69
+ <h2 className="ub-head">{t("account.recentOrders")}</h2>
70
+ {!!orders?.length && (
71
+ <Link href="/account/orders" className="ub-acc__more">
72
+ {t("section.viewAll")} <Icon name="arrowRight" />
73
+ </Link>
74
+ )}
75
+ </div>
76
+ <ul className="ub-acc__orders" data-testid="orders-wrapper">
77
+ {orders && orders.length > 0 ? (
78
+ orders.slice(0, 5).map((order) => (
79
+ <li key={order.id} data-testid="order-wrapper" data-value={order.id}>
80
+ <Link href={`/account/orders/details/${order.id}`} className="ub-acc__order" data-testid="open-order-button">
81
+ <span className="ub-acc__ic">
82
+ <Icon name="truck" />
83
+ </span>
84
+ <span>
85
+ <b data-testid="order-id" data-value={order.display_id}>
86
+ #{order.display_id}
87
+ </b>
88
+ <small data-testid="order-created-date">{date.format(new Date(order.created_at))}</small>
89
+ </span>
90
+ <span className="ub-acc__amount" data-testid="order-amount">
91
+ {convertToLocale({ amount: order.total, currency_code: order.currency_code })}
92
+ </span>
93
+ <Icon name="arrowRight" />
94
+ </Link>
95
+ </li>
96
+ ))
97
+ ) : (
98
+ <li className="ub-acc__empty" data-testid="no-orders-container">
99
+ <span className="ub-acc__ic">
100
+ <Icon name="cart" />
101
+ </span>
102
+ <b>{t("account.noRecentOrders")}</b>
103
+ <Link href="/store" className="ub-acc__shop">
104
+ {t("account.keepShopping")} <Icon name="arrowRight" />
105
+ </Link>
106
+ </li>
107
+ )}
108
+ </ul>
109
+ </section>
135
110
  </div>
136
111
  )
137
112
  }
138
113
 
139
114
  const getProfileCompletion = (customer: HttpTypes.StoreCustomer | null) => {
115
+ if (!customer) return 0
140
116
  let count = 0
141
-
142
- if (!customer) {
143
- return 0
144
- }
145
-
146
- if (customer.email) {
147
- count++
148
- }
149
-
150
- if (customer.first_name && customer.last_name) {
151
- count++
152
- }
153
-
154
- if (customer.phone) {
155
- count++
156
- }
157
-
158
- const billingAddress = customer.addresses?.find(
159
- (addr) => addr.is_default_billing
160
- )
161
-
162
- if (billingAddress) {
163
- count++
164
- }
165
-
117
+ if (customer.email) count++
118
+ if (customer.first_name && customer.last_name) count++
119
+ if (customer.phone) count++
120
+ if (customer.addresses?.find((addr) => addr.is_default_billing)) count++
166
121
  return (count / 4) * 100
167
122
  }
168
123
 
@@ -4,6 +4,7 @@ import React, { useEffect, useMemo, useActionState } from "react"
4
4
 
5
5
  import { useT } from "@lib/boutique/i18n-client"
6
6
  import Input from "@modules/common/components/input"
7
+ import PhoneInput from "@modules/common/components/phone-input"
7
8
  import NativeSelect from "@modules/common/components/native-select"
8
9
 
9
10
  import AccountInfo from "../account-info"
@@ -125,11 +126,9 @@ const ProfileBillingAddress: React.FC<MyInformationProps> = ({
125
126
  defaultValue={billingAddress?.company || undefined}
126
127
  data-testid="billing-company-input"
127
128
  />
128
- <Input
129
+ <PhoneInput
129
130
  label="form.phone"
130
131
  name="phone"
131
- type="phone"
132
- autoComplete="phone"
133
132
  required
134
133
  defaultValue={billingAddress?.phone ?? customer?.phone ?? ""}
135
134
  data-testid="billing-phone-input"
@@ -3,6 +3,7 @@
3
3
  import React, { useEffect, useActionState } from "react";
4
4
 
5
5
  import Input from "@modules/common/components/input"
6
+ import PhoneInput from "@modules/common/components/phone-input"
6
7
 
7
8
  import AccountInfo from "../account-info"
8
9
  import { HttpTypes } from "@medusajs/types"
@@ -56,11 +57,9 @@ const ProfileEmail: React.FC<MyInformationProps> = ({ customer }) => {
56
57
  data-testid="account-phone-editor"
57
58
  >
58
59
  <div className="grid grid-cols-1 gap-y-2">
59
- <Input
60
+ <PhoneInput
60
61
  label="form.phone"
61
62
  name="phone"
62
- type="phone"
63
- autoComplete="phone"
64
63
  required
65
64
  defaultValue={customer.phone ?? ""}
66
65
  data-testid="phone-input"
@@ -4,6 +4,7 @@
4
4
  import { T } from "@lib/boutique/i18n-client"
5
5
  import { useActionState } from "react"
6
6
  import Input from "@modules/common/components/input"
7
+ import PhoneInput from "@modules/common/components/phone-input"
7
8
  import { LOGIN_VIEW } from "@modules/account/templates/login-template"
8
9
  import ErrorMessage from "@modules/checkout/components/error-message"
9
10
  import { SubmitButton } from "@modules/checkout/components/submit-button"
@@ -52,11 +53,9 @@ const Register = ({ setCurrentView }: Props) => {
52
53
  autoComplete="email"
53
54
  data-testid="email-input"
54
55
  />
55
- <Input
56
+ <PhoneInput
56
57
  label="form.phone"
57
58
  name="phone"
58
- type="tel"
59
- autoComplete="tel"
60
59
  data-testid="phone-input"
61
60
  />
62
61
  <Input
@@ -1,40 +1,36 @@
1
1
  import { T } from "@lib/boutique/i18n-client"
2
2
  import React from "react"
3
3
 
4
- import UnderlineLink from "@modules/common/components/interactive-link"
4
+ import LocalizedClientLink from "@modules/common/components/localized-client-link"
5
+ import { HttpTypes } from "@medusajs/types"
5
6
 
6
7
  import AccountNav from "../components/account-nav"
7
- import { HttpTypes } from "@medusajs/types"
8
8
 
9
9
  interface AccountLayoutProps {
10
10
  customer: HttpTypes.StoreCustomer | null
11
11
  children: React.ReactNode
12
12
  }
13
13
 
14
- const AccountLayout: React.FC<AccountLayoutProps> = ({
15
- customer,
16
- children,
17
- }) => {
14
+ /** Espace client : menu latéral (connecté) + contenu, puis carte d'aide. Non connecté : formulaire centré. */
15
+ const AccountLayout: React.FC<AccountLayoutProps> = ({ customer, children }) => {
18
16
  return (
19
- <div className="flex-1 small:py-12" data-testid="account-page">
20
- <div className="flex-1 content-container h-full max-w-5xl mx-auto bg-white flex flex-col">
21
- <div className="grid grid-cols-1 small:grid-cols-[240px_1fr] py-12">
22
- <div>{customer && <AccountNav customer={customer} />}</div>
23
- <div className="flex-1">{children}</div>
24
- </div>
25
- <div className="flex flex-col small:flex-row items-end justify-between small:border-t border-gray-200 py-12 gap-8">
26
- <div>
27
- <h3 className="text-xl-semi mb-4"><T k="account.questions" /></h3>
28
- <span className="txt-medium">
29
- <T k="account.questionsText" />
30
- </span>
31
- </div>
32
- <div>
33
- <UnderlineLink href="/legal/contact">
34
- <T k="account.contactUs" />
35
- </UnderlineLink>
36
- </div>
17
+ <div className="ub-wrap ub-account" data-testid="account-page">
18
+ <div className={customer ? "ub-account__grid" : "ub-account__solo"}>
19
+ {customer && <AccountNav customer={customer} />}
20
+ <div className="ub-account__main">{children}</div>
21
+ </div>
22
+ <div className="ub-account__help">
23
+ <div>
24
+ <h3 className="ub-head">
25
+ <T k="account.questions" />
26
+ </h3>
27
+ <p>
28
+ <T k="account.questionsText" />
29
+ </p>
37
30
  </div>
31
+ <LocalizedClientLink href="/legal/contact" className="ub-btn ub-btn--primary">
32
+ <T k="account.contactUs" />
33
+ </LocalizedClientLink>
38
34
  </div>
39
35
  </div>
40
36
  )
@@ -1,5 +1,6 @@
1
1
  import { HttpTypes } from "@medusajs/types"
2
2
  import Input from "@modules/common/components/input"
3
+ import PhoneInput from "@modules/common/components/phone-input"
3
4
  import React, { useState } from "react"
4
5
  import CountrySelect from "../country-select"
5
6
 
@@ -98,10 +99,9 @@ const BillingAddress = ({ cart }: { cart: HttpTypes.StoreCart | null }) => {
98
99
  onChange={handleChange}
99
100
  data-testid="billing-province-input"
100
101
  />
101
- <Input
102
+ <PhoneInput
102
103
  label="form.phone"
103
104
  name="billing_address.phone"
104
- autoComplete="tel"
105
105
  value={formData["billing_address.phone"]}
106
106
  onChange={handleChange}
107
107
  data-testid="billing-phone-input"
@@ -10,6 +10,7 @@ import { Button, Container, Heading, Text, clx } from "@medusajs/ui"
10
10
  import ErrorMessage from "@modules/checkout/components/error-message"
11
11
  import PaymentContainer, {
12
12
  StripeCardContainer,
13
+ paymentTypeLabel,
13
14
  } from "@modules/checkout/components/payment-container"
14
15
  import Divider from "@modules/common/components/divider"
15
16
  import { usePathname, useRouter, useSearchParams } from "next/navigation"
@@ -232,7 +233,7 @@ const Payment = ({
232
233
  </Container>
233
234
  <Text>
234
235
  {isStripeLike(selectedPaymentMethod) && cardBrand
235
- ? cardBrand
236
+ ? paymentTypeLabel(cardBrand)
236
237
  : <T k="checkout.anotherStep" />}
237
238
  </Text>
238
239
  </div>
@@ -7,7 +7,8 @@ import { placeOrder } from "@lib/data/cart"
7
7
  import { HttpTypes } from "@medusajs/types"
8
8
  import { Button } from "@medusajs/ui"
9
9
  import { useElements, useStripe } from "@stripe/react-stripe-js"
10
- import React, { useState } from "react"
10
+ import { useSearchParams } from "next/navigation"
11
+ import React, { useEffect, useRef, useState } from "react"
11
12
  import ErrorMessage from "../error-message"
12
13
 
13
14
  type PaymentButtonProps = {
@@ -27,8 +28,15 @@ const PaymentButton: React.FC<PaymentButtonProps> = ({
27
28
  (cart.shipping_methods?.length ?? 0) < 1
28
29
 
29
30
  const paymentSession = cart.payment_collection?.payment_sessions?.[0]
31
+ const redirectStatus = useSearchParams().get("redirect_status")
30
32
 
31
33
  switch (true) {
34
+ case isStripeLike(paymentSession?.provider_id) && !!redirectStatus:
35
+ return (
36
+ <StripeRedirectReturn status={redirectStatus!}>
37
+ <Button size="large" disabled><T k="checkout.placeOrder" /></Button>
38
+ </StripeRedirectReturn>
39
+ )
32
40
  case isStripeLike(paymentSession?.provider_id):
33
41
  return (
34
42
  <StripePaymentButton
@@ -46,6 +54,32 @@ const PaymentButton: React.FC<PaymentButtonProps> = ({
46
54
  }
47
55
  }
48
56
 
57
+ /** Retour d'un moyen de paiement à redirection (Klarna, banque…) : Stripe renvoie sur /checkout?step=review&redirect_status=… */
58
+ const StripeRedirectReturn = ({ status, children }: { status: string; children: React.ReactNode }) => {
59
+ const [errorMessage, setErrorMessage] = useState<string | null>(null)
60
+ const started = useRef(false)
61
+
62
+ useEffect(() => {
63
+ if (status !== "succeeded" || started.current) return
64
+ started.current = true
65
+ placeOrder().catch((err) => setErrorMessage(err.message))
66
+ }, [status])
67
+
68
+ if (status === "succeeded" && !errorMessage) return <Button size="large" isLoading disabled><T k="checkout.placeOrder" /></Button>
69
+ return (
70
+ <>
71
+ {children}
72
+ {errorMessage ? (
73
+ <ErrorMessage error={errorMessage} data-testid="stripe-payment-error-message" />
74
+ ) : (
75
+ <p className="text-small-regular text-ui-fg-subtle mt-2" role="status">
76
+ <T k={status === "processing" ? "checkout.paymentPending" : "checkout.paymentFailed"} />
77
+ </p>
78
+ )}
79
+ </>
80
+ )
81
+ }
82
+
49
83
  const StripePaymentButton = ({
50
84
  cart,
51
85
  notReady,
@@ -70,68 +104,51 @@ const StripePaymentButton = ({
70
104
 
71
105
  const stripe = useStripe()
72
106
  const elements = useElements()
73
- const card = elements?.getElement("card")
74
-
75
- const session = cart.payment_collection?.payment_sessions?.find(
76
- (s) => s.status === "pending"
77
- )
78
107
 
79
108
  const disabled = !stripe || !elements ? true : false
80
109
 
110
+ // Stripe Payment Element : carte, Apple Pay, Google Pay… Aucune donnée de paiement ne passe par nos serveurs ;
111
+ // le montant est celui du PaymentIntent créé par Medusa. Pas de redirection sauf moyen qui l'exige.
81
112
  const handlePayment = async () => {
82
113
  setSubmitting(true)
114
+ setErrorMessage(null)
83
115
 
84
- if (!stripe || !elements || !card || !cart) {
116
+ if (!stripe || !elements || !cart) {
85
117
  setSubmitting(false)
86
118
  return
87
119
  }
88
120
 
89
- await stripe
90
- .confirmCardPayment(session?.data.client_secret as string, {
91
- payment_method: {
92
- card: card,
121
+ const address = cart.billing_address
122
+ const { error, paymentIntent } = await stripe.confirmPayment({
123
+ elements,
124
+ redirect: "if_required",
125
+ confirmParams: {
126
+ return_url: `${window.location.origin}/checkout?step=review`,
127
+ payment_method_data: {
93
128
  billing_details: {
94
- name:
95
- cart.billing_address?.first_name +
96
- " " +
97
- cart.billing_address?.last_name,
129
+ name: [address?.first_name, address?.last_name].filter(Boolean).join(" ") || undefined,
130
+ email: cart.email ?? undefined,
131
+ phone: address?.phone || undefined,
98
132
  address: {
99
- city: cart.billing_address?.city ?? undefined,
100
- country: cart.billing_address?.country_code ?? undefined,
101
- line1: cart.billing_address?.address_1 ?? undefined,
102
- line2: cart.billing_address?.address_2 ?? undefined,
103
- postal_code: cart.billing_address?.postal_code ?? undefined,
104
- state: cart.billing_address?.province ?? undefined,
133
+ city: address?.city ?? undefined,
134
+ country: address?.country_code ?? undefined,
135
+ line1: address?.address_1 ?? undefined,
136
+ line2: address?.address_2 ?? undefined,
137
+ postal_code: address?.postal_code ?? undefined,
138
+ state: address?.province ?? undefined,
105
139
  },
106
- email: cart.email,
107
- phone: cart.billing_address?.phone ?? undefined,
108
140
  },
109
141
  },
110
- })
111
- .then(({ error, paymentIntent }) => {
112
- if (error) {
113
- const pi = error.payment_intent
114
-
115
- if (
116
- (pi && pi.status === "requires_capture") ||
117
- (pi && pi.status === "succeeded")
118
- ) {
119
- onPaymentCompleted()
120
- }
121
-
122
- setErrorMessage(error.message || null)
123
- return
124
- }
125
-
126
- if (
127
- (paymentIntent && paymentIntent.status === "requires_capture") ||
128
- paymentIntent.status === "succeeded"
129
- ) {
130
- return onPaymentCompleted()
131
- }
132
-
133
- return
134
- })
142
+ },
143
+ })
144
+
145
+ const intent = paymentIntent ?? (error && "payment_intent" in error ? error.payment_intent : undefined)
146
+ if (intent && (intent.status === "succeeded" || intent.status === "requires_capture")) {
147
+ return onPaymentCompleted()
148
+ }
149
+
150
+ setErrorMessage(error?.message ?? null)
151
+ setSubmitting(false)
135
152
  }
136
153
 
137
154
  return (