@shoppexio/storefront-react 0.3.0 → 0.4.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/CHANGELOG.md +23 -0
- package/dist/account.css +4 -8
- package/dist/account.js +1257 -778
- package/dist/account.js.map +1 -1
- package/dist/index.d.ts +92 -8
- package/dist/index.js +15 -16
- package/dist/index.js.map +1 -1
- package/eject/account/account-downloads.ts +89 -1
- package/eject/account/account-referrals.ts +70 -1
- package/eject/account/account-reseller.ts +6 -140
- package/eject/account/downloads.tsx +73 -4
- package/eject/account/hosted-page.ts +45 -0
- package/eject/account/messages/ar.json +55 -29
- package/eject/account/messages/de.json +55 -29
- package/eject/account/messages/en.json +55 -29
- package/eject/account/messages/es.json +55 -29
- package/eject/account/messages/fi.json +55 -29
- package/eject/account/messages/fr.json +55 -29
- package/eject/account/messages/hi.json +55 -29
- package/eject/account/messages/it.json +55 -29
- package/eject/account/messages/lt.json +55 -29
- package/eject/account/messages/nl.json +55 -29
- package/eject/account/messages/pl.json +55 -29
- package/eject/account/messages/pt.json +55 -29
- package/eject/account/messages/ru.json +55 -29
- package/eject/account/messages/tr.json +55 -29
- package/eject/account/messages/uk.json +55 -29
- package/eject/account/messages/zh.json +55 -29
- package/eject/account/orders.tsx +88 -0
- package/eject/account/overview.tsx +6 -0
- package/eject/account/referrals.tsx +47 -5
- package/eject/account/reseller.tsx +59 -306
- package/eject/account/rewards.tsx +5 -0
- package/eject/account/shared.tsx +39 -1
- package/eject/account/subscriptions.tsx +75 -16
- package/package.json +4 -4
package/eject/account/orders.tsx
CHANGED
|
@@ -445,6 +445,9 @@ export function AccountOrderDetail({ orderId }: { orderId: string }) {
|
|
|
445
445
|
order AFTER it was paid for.
|
|
446
446
|
*/}
|
|
447
447
|
<OrderWarranties orderUniqid={order.uniqid} />
|
|
448
|
+
|
|
449
|
+
{/* Keyed: a review posted on one order must not follow the buyer to the next. */}
|
|
450
|
+
<OrderReview key={order.uniqid} orderUniqid={order.uniqid} />
|
|
448
451
|
</div>
|
|
449
452
|
);
|
|
450
453
|
}
|
|
@@ -686,6 +689,91 @@ function WarrantyRow({
|
|
|
686
689
|
);
|
|
687
690
|
}
|
|
688
691
|
|
|
692
|
+
/** The three scores the server keeps, as the buyer reads them. */
|
|
693
|
+
const REVIEW_SCORE_KEYS: Readonly<Record<number, string>> = {
|
|
694
|
+
5: 'orders.reviewPositive',
|
|
695
|
+
3: 'orders.reviewNeutral',
|
|
696
|
+
1: 'orders.reviewNegative',
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* The buyer's review of this order.
|
|
701
|
+
*
|
|
702
|
+
* The server decides whether the order can be reviewed (`can_review`: paid,
|
|
703
|
+
* reviews on, inside the edit window) and names the hosted review page for it
|
|
704
|
+
* (`review_url`, null otherwise); this only reads that state and never guesses
|
|
705
|
+
* it. The review is written on that page, never posted from here: this page
|
|
706
|
+
* runs beside merchant theme code that holds the buyer's session, and a review
|
|
707
|
+
* it could post would not be the buyer's own (#1152). Nothing is drawn while
|
|
708
|
+
* the read is in flight, or when there is neither a review page to open nor a
|
|
709
|
+
* review of the buyer's own to show. A failed read is shown with a retry,
|
|
710
|
+
* because it cannot be told apart from an order waiting for its review.
|
|
711
|
+
*/
|
|
712
|
+
export function OrderReview({ orderUniqid }: { orderUniqid: string }) {
|
|
713
|
+
const { state, reload } = useAccountResource(`review:${orderUniqid}`, () =>
|
|
714
|
+
callCustomerApi((sdk) => sdk.orderReview(orderUniqid)));
|
|
715
|
+
|
|
716
|
+
if (state.status === 'loading') return null;
|
|
717
|
+
|
|
718
|
+
if (state.status === 'error') {
|
|
719
|
+
return (
|
|
720
|
+
<ReviewSection>
|
|
721
|
+
<AccountError message={state.message} onRetry={reload} />
|
|
722
|
+
</ReviewSection>
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const review = state.data;
|
|
727
|
+
const ownReview = review.already_submitted && !review.is_automated;
|
|
728
|
+
// Absent on a backend that predates the link; a URL that fails the scheme check gets no anchor.
|
|
729
|
+
const href = safeExternalHref(review.review_url);
|
|
730
|
+
if (href === null && !ownReview) return null;
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
<ReviewSection
|
|
734
|
+
description={review.product_title
|
|
735
|
+
? t('orders.reviewPrompt', { product: review.product_title })
|
|
736
|
+
: t('orders.reviewPromptGeneric')}
|
|
737
|
+
>
|
|
738
|
+
<AccountCard className="sxa:flex sxa:flex-col sxa:items-start sxa:gap-3 sxa:px-5 sxa:py-4">
|
|
739
|
+
{ownReview ? (
|
|
740
|
+
<div>
|
|
741
|
+
<p className="sxa:text-small sxa:font-semibold">
|
|
742
|
+
{t((review.score === null ? undefined : REVIEW_SCORE_KEYS[review.score]) ?? 'orders.reviewFallbackLabel')}
|
|
743
|
+
{review.submitted_at ? (
|
|
744
|
+
<span className="sxa:font-normal sxa:text-muted"> · {formatAccountDate(review.submitted_at)}</span>
|
|
745
|
+
) : null}
|
|
746
|
+
</p>
|
|
747
|
+
{review.message ? (
|
|
748
|
+
<p className="sxa:mt-1.5 sxa:text-small sxa:whitespace-pre-line sxa:text-muted">{review.message}</p>
|
|
749
|
+
) : null}
|
|
750
|
+
</div>
|
|
751
|
+
) : review.is_automated ? (
|
|
752
|
+
<p className="sxa:text-small sxa:text-muted">{t('orders.reviewAutomated')}</p>
|
|
753
|
+
) : null}
|
|
754
|
+
{href === null ? null : (
|
|
755
|
+
<a
|
|
756
|
+
href={href}
|
|
757
|
+
className="sx-account-cta sxa:cta-button sxa:inline-flex sxa:h-9 sxa:shrink-0 sxa:items-center sxa:px-4 sxa:text-small sxa:font-semibold"
|
|
758
|
+
>
|
|
759
|
+
{t(ownReview ? 'orders.reviewEdit' : 'orders.reviewWrite')}
|
|
760
|
+
</a>
|
|
761
|
+
)}
|
|
762
|
+
</AccountCard>
|
|
763
|
+
</ReviewSection>
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function ReviewSection({ description, children }: { description?: string; children: ReactNode }) {
|
|
768
|
+
return (
|
|
769
|
+
<section className="sxa:mt-8">
|
|
770
|
+
<AccountSectionTitle>{t('orders.reviewTitle')}</AccountSectionTitle>
|
|
771
|
+
{description ? <p className="sxa:mt-1.5 sxa:text-small sxa:text-muted">{description}</p> : null}
|
|
772
|
+
<div className="sxa:mt-4">{children}</div>
|
|
773
|
+
</section>
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
|
|
689
777
|
/** Cover as a sentence. Cancelled cover names no date, because it has none. */
|
|
690
778
|
function coverageLabel(coverage: WarrantyCoverage): string {
|
|
691
779
|
switch (coverage.kind) {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
AccountError,
|
|
22
22
|
AccountRowsSkeleton,
|
|
23
23
|
AccountSectionTitle,
|
|
24
|
+
HostedPageLink,
|
|
24
25
|
formatAccountAmount,
|
|
25
26
|
formatInvoiceAmount,
|
|
26
27
|
} from './shared';
|
|
@@ -78,6 +79,11 @@ export function AccountOverview() {
|
|
|
78
79
|
</div>
|
|
79
80
|
<p className="sxa:text-small sxa:text-muted">{t('overview.walletNote')}</p>
|
|
80
81
|
</div>
|
|
82
|
+
{/* Adding funds is a payment, so it happens on the hosted top-up page,
|
|
83
|
+
and only when the shop takes top-ups (decision T4). */}
|
|
84
|
+
{account.wallet.topups_allowed ? (
|
|
85
|
+
<HostedPageLink page="topup" hints={{}} label={t('overview.topUp')} className="sxa:mt-4 sxa:border-t sxa:pt-4" />
|
|
86
|
+
) : null}
|
|
81
87
|
</AccountCard>
|
|
82
88
|
) : null}
|
|
83
89
|
|
|
@@ -24,13 +24,15 @@ import {
|
|
|
24
24
|
isEnrolledInReferrals,
|
|
25
25
|
isForeignBalanceCurrency,
|
|
26
26
|
isShareableLink,
|
|
27
|
+
linkCommissionDiffers,
|
|
27
28
|
offersReferralLinks,
|
|
28
29
|
referralLinkInput,
|
|
30
|
+
referralRateRows,
|
|
29
31
|
referralShareUrl,
|
|
30
32
|
} from './account-referrals';
|
|
31
33
|
import { callCustomerApi, safeExternalHref } from './customer-account';
|
|
32
|
-
import type { SdkCustomerAffiliateLink, SdkCustomerAffiliateSummary } from '@shoppexio/storefront-react';
|
|
33
|
-
import { t } from './i18n';
|
|
34
|
+
import type { SdkCustomerAffiliateLink, SdkCustomerAffiliateRates, SdkCustomerAffiliateSummary } from '@shoppexio/storefront-react';
|
|
35
|
+
import { getActiveLocale, t } from './i18n';
|
|
34
36
|
import { ExternalLinkIcon } from './icons';
|
|
35
37
|
import { useAccountDashboard } from './dashboard';
|
|
36
38
|
import {
|
|
@@ -136,6 +138,8 @@ export function AccountReferrals() {
|
|
|
136
138
|
</div>
|
|
137
139
|
) : null}
|
|
138
140
|
|
|
141
|
+
{summary.rates ? <YourRates rates={summary.rates} /> : null}
|
|
142
|
+
|
|
139
143
|
<section>
|
|
140
144
|
<div className="sxa:flex sxa:flex-wrap sxa:items-center sxa:justify-between sxa:gap-3">
|
|
141
145
|
<AccountSectionTitle>{t('referrals.linksTitle')}</AccountSectionTitle>
|
|
@@ -192,7 +196,7 @@ export function AccountReferrals() {
|
|
|
192
196
|
<ul className="sxa:space-y-4">
|
|
193
197
|
{summary.links.map((link) => (
|
|
194
198
|
<li key={link.id}>
|
|
195
|
-
<ReferralLinkCard link={link} currency={currency} />
|
|
199
|
+
<ReferralLinkCard link={link} currency={currency} rates={summary.rates} />
|
|
196
200
|
</li>
|
|
197
201
|
))}
|
|
198
202
|
</ul>
|
|
@@ -460,12 +464,45 @@ function BalanceTile({
|
|
|
460
464
|
);
|
|
461
465
|
}
|
|
462
466
|
|
|
467
|
+
/** "Your rates": what a referral earns, in the rows the Easy-lane account block shows. */
|
|
468
|
+
function YourRates({ rates }: { rates: SdkCustomerAffiliateRates }) {
|
|
469
|
+
const id = useId();
|
|
470
|
+
const locale = getActiveLocale();
|
|
471
|
+
const rows = referralRateRows(rates, {
|
|
472
|
+
// The cap and the minimum are kept in US dollars, like the affiliate ledger.
|
|
473
|
+
money: (usd) => formatAccountAmount(usd, 'USD'),
|
|
474
|
+
list: (names) => new Intl.ListFormat(locale, { type: 'conjunction' }).format(names),
|
|
475
|
+
more: (count) => t('referrals.ratesMoreProducts', { count: String(count) }),
|
|
476
|
+
});
|
|
477
|
+
return (
|
|
478
|
+
<section aria-labelledby={id}>
|
|
479
|
+
<h2 id={id} className="sx-account-section-title sxa:font-heading sxa:text-body sxa:font-semibold">
|
|
480
|
+
{t('referrals.yourRates')}
|
|
481
|
+
</h2>
|
|
482
|
+
<AccountCard className="sxa:mt-4 sxa:p-5">
|
|
483
|
+
<ul className="sxa:space-y-3">
|
|
484
|
+
{rows.map((row) => (
|
|
485
|
+
<li key={row.id}>
|
|
486
|
+
<p className="sxa:text-small sxa:font-semibold">{t(row.title.key, row.title.params)}</p>
|
|
487
|
+
{row.detail ? (
|
|
488
|
+
<p className="sxa:mt-0.5 sxa:text-micro sxa:text-muted">{t(row.detail.key, row.detail.params)}</p>
|
|
489
|
+
) : null}
|
|
490
|
+
</li>
|
|
491
|
+
))}
|
|
492
|
+
</ul>
|
|
493
|
+
</AccountCard>
|
|
494
|
+
</section>
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
463
498
|
function ReferralLinkCard({
|
|
464
499
|
link,
|
|
465
500
|
currency,
|
|
501
|
+
rates,
|
|
466
502
|
}: {
|
|
467
503
|
link: SdkCustomerAffiliateLink;
|
|
468
504
|
currency: string;
|
|
505
|
+
rates: SdkCustomerAffiliateRates | null;
|
|
469
506
|
}) {
|
|
470
507
|
/*
|
|
471
508
|
* The shareable URL is the SERVER's — it knows the shop's own storefront
|
|
@@ -490,8 +527,13 @@ function ReferralLinkCard({
|
|
|
490
527
|
<div className="sxa:min-w-0">
|
|
491
528
|
<p className="sxa:truncate sxa:text-small sxa:font-semibold">{link.label ?? link.code}</p>
|
|
492
529
|
<p className="sxa:mt-1 sxa:text-micro sxa:text-muted">
|
|
493
|
-
{
|
|
494
|
-
{
|
|
530
|
+
{/* "Your rates" names the rate; a link says it only when it earns another one. */}
|
|
531
|
+
{linkCommissionDiffers(link, rates) ? (
|
|
532
|
+
<>
|
|
533
|
+
{t('referrals.commission', { percent: link.effective_commission_percent })}
|
|
534
|
+
{' · '}
|
|
535
|
+
</>
|
|
536
|
+
) : null}
|
|
495
537
|
{t('referrals.buyerDiscount', { percent: link.effective_customer_discount_percent })}
|
|
496
538
|
</p>
|
|
497
539
|
</div>
|