@shoppexio/storefront-react 0.4.0 → 0.5.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 +8 -0
- package/dist/account.css +22 -0
- package/dist/account.js +2031 -294
- package/dist/account.js.map +1 -1
- package/dist/index.d.ts +108 -18
- package/dist/index.js +29 -5
- package/dist/index.js.map +1 -1
- package/eject/account/account-payment-method.ts +1 -0
- package/eject/account/account-subscriptions.ts +158 -6
- package/eject/account/buyer-invoice-status.ts +100 -0
- package/eject/account/downloads.tsx +45 -13
- package/eject/account/messages/ar.json +99 -14
- package/eject/account/messages/de.json +99 -14
- package/eject/account/messages/en.json +99 -14
- package/eject/account/messages/es.json +99 -14
- package/eject/account/messages/fi.json +99 -14
- package/eject/account/messages/fr.json +99 -14
- package/eject/account/messages/hi.json +99 -14
- package/eject/account/messages/it.json +99 -14
- package/eject/account/messages/lt.json +99 -14
- package/eject/account/messages/nl.json +99 -14
- package/eject/account/messages/pl.json +99 -14
- package/eject/account/messages/pt.json +99 -14
- package/eject/account/messages/ru.json +99 -14
- package/eject/account/messages/tr.json +99 -14
- package/eject/account/messages/uk.json +99 -14
- package/eject/account/messages/zh.json +99 -14
- package/eject/account/orders.tsx +115 -41
- package/eject/account/reseller.tsx +3 -2
- package/eject/account/shared.tsx +21 -3
- package/eject/account/subscriptions.tsx +324 -46
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SdkCustomerSubscription } from '@shoppexio/storefront-react';
|
|
1
|
+
import type { SdkCustomerOpenRenewal, SdkCustomerPauseOptions, SdkCustomerSubscription } from '@shoppexio/storefront-react';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* The subscriptions panel's pure half: which date matters, how the plan bills,
|
|
@@ -14,17 +14,38 @@ import type { SdkCustomerSubscription } from '@shoppexio/storefront-react';
|
|
|
14
14
|
* renewal it will never take.
|
|
15
15
|
*/
|
|
16
16
|
export type SubscriptionTiming =
|
|
17
|
+
| { kind: 'ended'; date: string }
|
|
17
18
|
| { kind: 'canceled'; date: string }
|
|
18
19
|
| { kind: 'ends'; date: string }
|
|
20
|
+
/** A renewal on hold or past its date: the deadline to pay it, not a renewal date already gone. */
|
|
21
|
+
| { kind: 'payBy'; date: string }
|
|
22
|
+
| { kind: 'pauses'; date: string }
|
|
23
|
+
| { kind: 'resumes'; date: string }
|
|
24
|
+
/** Paused by the merchant or the API without a resume date. */
|
|
25
|
+
| { kind: 'pausedOpenEnded'; date: string }
|
|
19
26
|
| { kind: 'trial'; date: string }
|
|
20
27
|
| { kind: 'renews'; date: string };
|
|
21
28
|
|
|
22
29
|
export function subscriptionTiming(subscription: SdkCustomerSubscription): SubscriptionTiming {
|
|
30
|
+
if (endedAfterHold(subscription) && subscription.canceled_at) {
|
|
31
|
+
return { kind: 'ended', date: subscription.canceled_at };
|
|
32
|
+
}
|
|
23
33
|
if (subscription.status.toUpperCase() === 'CANCELED' && subscription.canceled_at) {
|
|
24
34
|
return { kind: 'canceled', date: subscription.canceled_at };
|
|
25
35
|
}
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
const renewal = openRenewalState(subscription);
|
|
37
|
+
if (subscription.open_renewal && (renewal === 'held' || renewal === 'due')) {
|
|
38
|
+
return { kind: 'payBy', date: subscription.open_renewal.pay_by };
|
|
39
|
+
}
|
|
40
|
+
// The producer's one "set to end" fact: cancelled at the period end, or a voided renewal.
|
|
41
|
+
if (subscription.ends_at) {
|
|
42
|
+
return { kind: 'ends', date: subscription.ends_at };
|
|
43
|
+
}
|
|
44
|
+
// A paused or pausing plan bills nothing at its period end; it bills again when it resumes.
|
|
45
|
+
const pause = subscription.pause;
|
|
46
|
+
if (pause?.state === 'scheduled') return { kind: 'pauses', date: pause.starts_at };
|
|
47
|
+
if (pause?.state === 'paused') {
|
|
48
|
+
return pause.resume_at === null ? { kind: 'pausedOpenEnded', date: '' } : { kind: 'resumes', date: pause.resume_at };
|
|
28
49
|
}
|
|
29
50
|
if (subscription.status.toUpperCase() === 'TRIALING' && subscription.trial_end) {
|
|
30
51
|
return { kind: 'trial', date: subscription.trial_end };
|
|
@@ -34,7 +55,24 @@ export function subscriptionTiming(subscription: SdkCustomerSubscription): Subsc
|
|
|
34
55
|
|
|
35
56
|
/** The catalog key for a timing, so the panel never spells one itself. */
|
|
36
57
|
export function subscriptionTimingKey(timing: SubscriptionTiming): string {
|
|
37
|
-
return `subscriptions.${timing.kind}`;
|
|
58
|
+
return timing.kind === 'ended' ? 'subscriptions.endedOn' : `subscriptions.${timing.kind}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A subscription that ended with its renewal still unpaid (the hold ran out, or the
|
|
63
|
+
* shop ended it during the hold): CANCELED with the `hold_ends_at` the hold left on
|
|
64
|
+
* the row. `ranOut` is read from the two producer timestamps, never from the date today.
|
|
65
|
+
*/
|
|
66
|
+
export function endedAfterHold(subscription: SdkCustomerSubscription): { ranOut: boolean } | null {
|
|
67
|
+
if (subscription.status.toUpperCase() !== 'CANCELED' || !subscription.hold_ends_at || !subscription.canceled_at) return null;
|
|
68
|
+
const canceledAt = producerTimestamp(subscription.canceled_at);
|
|
69
|
+
const holdEndsAt = producerTimestamp(subscription.hold_ends_at);
|
|
70
|
+
return { ranOut: canceledAt !== null && holdEndsAt !== null && canceledAt >= holdEndsAt };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** The catalog key of the ended note's sentence. */
|
|
74
|
+
export function endedNoteKey(ended: { ranOut: boolean }): string {
|
|
75
|
+
return ended.ranOut ? 'subscriptions.endedHoldRanOut' : 'subscriptions.endedShopEnded';
|
|
38
76
|
}
|
|
39
77
|
|
|
40
78
|
const KNOWN_INTERVALS: ReadonlySet<string> = new Set(['day', 'week', 'month', 'year']);
|
|
@@ -82,6 +120,44 @@ export interface BillingHistoryInvoice {
|
|
|
82
120
|
currency: string;
|
|
83
121
|
status: string;
|
|
84
122
|
created_at: string;
|
|
123
|
+
/** What the invoice billed: the order that started the plan, a renewal, or a plan change. */
|
|
124
|
+
kind: 'first_order' | 'renewal' | 'plan_change';
|
|
125
|
+
/** The plan change it paid for; null on a renewal or a first order. Days are null when the producer did not count them. */
|
|
126
|
+
plan_change: { from_plan_name: string; to_plan_name: string; days_left: number | null; days_total: number | null } | null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const BILLING_KINDS: ReadonlyArray<BillingHistoryInvoice['kind']> = ['first_order', 'renewal', 'plan_change'];
|
|
130
|
+
|
|
131
|
+
function readCount(record: Record<string, unknown>, key: string): number | null | undefined {
|
|
132
|
+
const value = record[key];
|
|
133
|
+
if (value === null) return null;
|
|
134
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function readPlanChange(value: unknown): BillingHistoryInvoice['plan_change'] | undefined {
|
|
138
|
+
if (value === null) return null;
|
|
139
|
+
if (!isRecord(value)) return undefined;
|
|
140
|
+
const from = readString(value, 'from_plan_name');
|
|
141
|
+
const to = readString(value, 'to_plan_name');
|
|
142
|
+
const daysLeft = readCount(value, 'days_left');
|
|
143
|
+
const daysTotal = readCount(value, 'days_total');
|
|
144
|
+
if (from === null || to === null || daysLeft === undefined || daysTotal === undefined) return undefined;
|
|
145
|
+
return { from_plan_name: from, to_plan_name: to, days_left: daysLeft, days_total: daysTotal };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** What a billing history row billed (Design Batch 9, C3), as a catalog key: "Renewal", "First order", "Plan change, Basic to Pro". */
|
|
149
|
+
export function billingEntryTitle(invoice: BillingHistoryInvoice): { key: string; params?: { from: string; to: string } } {
|
|
150
|
+
if (invoice.plan_change) {
|
|
151
|
+
return { key: 'subscriptions.historyPlanChange', params: { from: invoice.plan_change.from_plan_name, to: invoice.plan_change.to_plan_name } };
|
|
152
|
+
}
|
|
153
|
+
return { key: invoice.kind === 'first_order' ? 'subscriptions.historyFirstOrder' : 'subscriptions.historyRenewal' };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** "for 12 of 30 days" beside a plan change's date, or null when there is nothing to count. */
|
|
157
|
+
export function billingEntryDays(invoice: BillingHistoryInvoice): { key: string; params: { days: number; total: number } } | null {
|
|
158
|
+
const change = invoice.plan_change;
|
|
159
|
+
if (!change || change.days_left === null || change.days_total === null) return null;
|
|
160
|
+
return { key: 'subscriptions.historyDays', params: { days: change.days_left, total: change.days_total } };
|
|
85
161
|
}
|
|
86
162
|
|
|
87
163
|
export type BillingHistoryResult =
|
|
@@ -118,13 +194,89 @@ export function narrowBillingHistory(payload: unknown): BillingHistoryResult {
|
|
|
118
194
|
const currency = readString(entry, 'currency');
|
|
119
195
|
const status = readString(entry, 'status');
|
|
120
196
|
const createdAt = readString(entry, 'created_at');
|
|
121
|
-
|
|
197
|
+
// A kind this build does not know fails the read: a row that guesses what it billed could misname a charge.
|
|
198
|
+
const kind = BILLING_KINDS.find((known) => known === entry.kind);
|
|
199
|
+
const planChange = readPlanChange(entry.plan_change);
|
|
200
|
+
if (uniqid === null || total === null || currency === null || status === null || createdAt === null || !kind || planChange === undefined) {
|
|
122
201
|
return { ok: false };
|
|
123
202
|
}
|
|
124
203
|
|
|
125
|
-
invoices.push({ uniqid, total, currency, status, created_at: createdAt });
|
|
204
|
+
invoices.push({ uniqid, total, currency, status, created_at: createdAt, kind, plan_change: planChange });
|
|
126
205
|
}
|
|
127
206
|
|
|
128
207
|
return { ok: true, invoices };
|
|
129
208
|
}
|
|
130
209
|
|
|
210
|
+
/** The length the pause dialog opens with: one month when the shop offers it, else the longest it offers. */
|
|
211
|
+
export function defaultPauseLength(options: SdkCustomerPauseOptions): SdkCustomerPauseOptions['lengths'][number]['days'] | null {
|
|
212
|
+
if (options.lengths.some((length) => length.days === 30)) return 30;
|
|
213
|
+
return options.lengths[options.lengths.length - 1]?.days ?? null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** "You can pause {n} more times…" as a catalog key and its count. */
|
|
217
|
+
export function pausesLeftMessage(left: number): { key: string; params: { count: number } } {
|
|
218
|
+
const key = left <= 0 ? 'subscriptions.pauseLeftNone' : left === 1 ? 'subscriptions.pauseLeftOne' : 'subscriptions.pauseLeftMany';
|
|
219
|
+
return { key, params: { count: left } };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Whether "Resume now" bills a new period today (`resumedPeriodEnd` in subscriptionPause.ts):
|
|
224
|
+
* a pause from the period end left no paid days, a "right away" pause gives the days left back.
|
|
225
|
+
*/
|
|
226
|
+
export function resumeBillsToday(subscription: SdkCustomerSubscription): boolean {
|
|
227
|
+
const pause = subscription.pause;
|
|
228
|
+
if (pause?.state !== 'paused') return false;
|
|
229
|
+
const periodEnd = producerTimestamp(subscription.current_period_end);
|
|
230
|
+
const startedAt = producerTimestamp(pause.starts_at);
|
|
231
|
+
return periodEnd !== null && startedAt !== null && periodEnd <= startedAt;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Where the open renewal stands, read only from the producer's `open_renewal`
|
|
236
|
+
* and `hold_ends_at`, never from guessed dates:
|
|
237
|
+
* - `held`: on hold (PAST_DUE with a hold end), access is off until the buyer pays;
|
|
238
|
+
* - `ready`: invoiced ahead, the renewal date is still to come;
|
|
239
|
+
* - `due`: the renewal date passed and the time to pay runs.
|
|
240
|
+
* Null when nothing is open, and on an ended subscription.
|
|
241
|
+
*/
|
|
242
|
+
export type OpenRenewalState = 'held' | 'due' | 'ready';
|
|
243
|
+
|
|
244
|
+
const NOTE_INTERVALS: Readonly<Record<string, string>> = { DAY: 'Day', WEEK: 'Week', MONTH: 'Month', YEAR: 'Year' };
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The unit inside a renewal note ("a new month"), from the plan the renewal invoice bills
|
|
248
|
+
* (a scheduled interval change bills its target before the subscription's own plan moves):
|
|
249
|
+
* only a plan that bills once per unit names it. Several units or an unknown unit read as
|
|
250
|
+
* the generic billing period, never as a unit the producer did not send.
|
|
251
|
+
*/
|
|
252
|
+
export function noteIntervalKey(open: SdkCustomerOpenRenewal, note: 'hold' | 'ready'): string {
|
|
253
|
+
const unit = open.interval_count === 1 ? NOTE_INTERVALS[open.interval.trim().toUpperCase()] : undefined;
|
|
254
|
+
return `subscriptions.${note}Interval${unit ?? 'Other'}`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function openRenewalState(subscription: SdkCustomerSubscription, now: number = Date.now()): OpenRenewalState | null {
|
|
258
|
+
const status = subscription.status.toUpperCase();
|
|
259
|
+
if (!subscription.open_renewal || status === 'CANCELED') return null;
|
|
260
|
+
if (status === 'PAST_DUE' && subscription.hold_ends_at) return 'held';
|
|
261
|
+
const renewsAt = producerTimestamp(subscription.current_period_end);
|
|
262
|
+
return renewsAt !== null && renewsAt > now ? 'ready' : 'due';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The renewal date for display: naive PostgreSQL strings ("2026-08-10 00:00:00.000")
|
|
267
|
+
* are UTC, not the buyer's local zone. An unreadable value comes back as it came.
|
|
268
|
+
*/
|
|
269
|
+
export function renewalDate(value: string): Date | string {
|
|
270
|
+
const parsed = producerTimestamp(value);
|
|
271
|
+
return parsed === null ? value : new Date(parsed);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function producerTimestamp(value: string): number | null {
|
|
275
|
+
const trimmed = value.trim();
|
|
276
|
+
if (!trimmed) return null;
|
|
277
|
+
const hasTimezone = /(?:Z|[+-]\d{2}(?::?\d{2})?)$/i.test(trimmed);
|
|
278
|
+
const hasTimePart = trimmed.includes(' ') || trimmed.includes('T');
|
|
279
|
+
const normalized = hasTimezone || !hasTimePart ? trimmed : `${trimmed.replace(' ', 'T')}Z`;
|
|
280
|
+
const parsed = Date.parse(normalized);
|
|
281
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
282
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One buyer status table (Design Batch 9, C2): what an invoice status means to
|
|
3
|
+
* the buyer, for every surface that states it — the hosted checkout, the theme
|
|
4
|
+
* account and the code-lane account.
|
|
5
|
+
*
|
|
6
|
+
* Data only, no strings. `labelKey` names the word; each lane maps it into its
|
|
7
|
+
* own message files (checkout `buyerStatus.*`, theme `account.status.*`, code
|
|
8
|
+
* lane `invoiceStatus.*`), so the same status reads the same word everywhere.
|
|
9
|
+
*
|
|
10
|
+
* tone positive done and good, notice your turn or still moving,
|
|
11
|
+
* danger a dispute, neutral over and nothing to act on
|
|
12
|
+
* paid the buyer's money arrived (AWAITING_STOCK included)
|
|
13
|
+
* receipt a receipt exists: paid, and not under a dispute
|
|
14
|
+
* live the order still moves without the buyer starting over
|
|
15
|
+
* filter the order-list group the status belongs to, or null (All only)
|
|
16
|
+
*
|
|
17
|
+
* Exhaustive over the backend's `InvoiceStatus` enum
|
|
18
|
+
* (apps/backend/drizzle/schema/enums.ts; parity test in apps/backend), so a new
|
|
19
|
+
* status cannot reach a buyer without a row. Nova ports this file
|
|
20
|
+
* (templates/code-storefronts/nova/src/commerce/buyer-invoice-status.ts); the
|
|
21
|
+
* parity test in this package keeps the two equal.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export const BUYER_INVOICE_STATUSES = [
|
|
25
|
+
'PENDING',
|
|
26
|
+
'PENDING_PAYMENT',
|
|
27
|
+
'PROCESSING',
|
|
28
|
+
'UNDERPAID',
|
|
29
|
+
'COMPLETED',
|
|
30
|
+
'VOIDED',
|
|
31
|
+
'REFUNDED',
|
|
32
|
+
'PARTIAL_REFUND',
|
|
33
|
+
'DISPUTED',
|
|
34
|
+
'EXPIRED',
|
|
35
|
+
'AWAITING_STOCK',
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export type BuyerInvoiceStatus = (typeof BUYER_INVOICE_STATUSES)[number];
|
|
39
|
+
|
|
40
|
+
export type BuyerStatusTone = 'positive' | 'notice' | 'danger' | 'neutral';
|
|
41
|
+
|
|
42
|
+
/** The order-list groups, in the order a filter lists them. The id is the wire value of `status_group`. */
|
|
43
|
+
export const BUYER_STATUS_FILTERS = [
|
|
44
|
+
{ id: 'awaiting_payment', labelKey: 'awaitingPayment' },
|
|
45
|
+
{ id: 'paid', labelKey: 'paid' },
|
|
46
|
+
{ id: 'refunded', labelKey: 'refunded' },
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
export type BuyerStatusFilter = (typeof BUYER_STATUS_FILTERS)[number]['id'];
|
|
50
|
+
|
|
51
|
+
export interface BuyerInvoiceStatusRow {
|
|
52
|
+
labelKey:
|
|
53
|
+
| 'awaitingPayment'
|
|
54
|
+
| 'processing'
|
|
55
|
+
| 'underpaid'
|
|
56
|
+
| 'awaitingStock'
|
|
57
|
+
| 'completed'
|
|
58
|
+
| 'partialRefund'
|
|
59
|
+
| 'refunded'
|
|
60
|
+
| 'disputed'
|
|
61
|
+
| 'expired'
|
|
62
|
+
| 'voided';
|
|
63
|
+
tone: BuyerStatusTone;
|
|
64
|
+
paid: boolean;
|
|
65
|
+
receipt: boolean;
|
|
66
|
+
live: boolean;
|
|
67
|
+
filter: BuyerStatusFilter | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Every word a lane needs a message for: the status labels and the filter labels. */
|
|
71
|
+
export type BuyerStatusLabelKey = BuyerInvoiceStatusRow['labelKey'] | (typeof BUYER_STATUS_FILTERS)[number]['labelKey'];
|
|
72
|
+
|
|
73
|
+
export const BUYER_INVOICE_STATUS: Readonly<Record<BuyerInvoiceStatus, Readonly<BuyerInvoiceStatusRow>>> = {
|
|
74
|
+
PENDING: { labelKey: 'awaitingPayment', tone: 'notice', paid: false, receipt: false, live: false, filter: 'awaiting_payment' },
|
|
75
|
+
PENDING_PAYMENT: { labelKey: 'awaitingPayment', tone: 'notice', paid: false, receipt: false, live: false, filter: 'awaiting_payment' },
|
|
76
|
+
PROCESSING: { labelKey: 'processing', tone: 'neutral', paid: false, receipt: false, live: true, filter: 'awaiting_payment' },
|
|
77
|
+
UNDERPAID: { labelKey: 'underpaid', tone: 'notice', paid: false, receipt: false, live: true, filter: 'awaiting_payment' },
|
|
78
|
+
// "Paid": the buyer has paid, the shop is restocking.
|
|
79
|
+
AWAITING_STOCK: { labelKey: 'awaitingStock', tone: 'notice', paid: true, receipt: true, live: true, filter: 'paid' },
|
|
80
|
+
COMPLETED: { labelKey: 'completed', tone: 'positive', paid: true, receipt: true, live: false, filter: 'paid' },
|
|
81
|
+
// A refund still needs a document, so the receipt stays.
|
|
82
|
+
PARTIAL_REFUND: { labelKey: 'partialRefund', tone: 'neutral', paid: true, receipt: true, live: false, filter: 'refunded' },
|
|
83
|
+
REFUNDED: { labelKey: 'refunded', tone: 'neutral', paid: true, receipt: true, live: false, filter: 'refunded' },
|
|
84
|
+
// A lost chargeback or a reversed capture: never paid, no receipt.
|
|
85
|
+
DISPUTED: { labelKey: 'disputed', tone: 'danger', paid: false, receipt: false, live: false, filter: null },
|
|
86
|
+
EXPIRED: { labelKey: 'expired', tone: 'neutral', paid: false, receipt: false, live: false, filter: null },
|
|
87
|
+
VOIDED: { labelKey: 'voided', tone: 'neutral', paid: false, receipt: false, live: false, filter: null },
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** The row for a producer status, or null for a status this table does not know. */
|
|
91
|
+
export function buyerInvoiceStatus(status: string | null | undefined): Readonly<BuyerInvoiceStatusRow> | null {
|
|
92
|
+
return status != null && Object.prototype.hasOwnProperty.call(BUYER_INVOICE_STATUS, status)
|
|
93
|
+
? BUYER_INVOICE_STATUS[status as BuyerInvoiceStatus]
|
|
94
|
+
: null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The statuses an order-list group holds. */
|
|
98
|
+
export function buyerStatusesInFilter(filter: BuyerStatusFilter): BuyerInvoiceStatus[] {
|
|
99
|
+
return BUYER_INVOICE_STATUSES.filter((status) => BUYER_INVOICE_STATUS[status].filter === filter);
|
|
100
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* clicked has no cause they could point at.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import type { BuyerInvoiceStatus } from './buyer-invoice-status';
|
|
18
19
|
import { useState } from 'react';
|
|
19
20
|
import { Link } from 'react-router-dom';
|
|
20
21
|
import { accountOrderHref } from '@shoppexio/storefront-react';
|
|
@@ -45,8 +46,51 @@ import {
|
|
|
45
46
|
CopyButton,
|
|
46
47
|
StatusChip,
|
|
47
48
|
formatAccountDate,
|
|
49
|
+
useAccountResource,
|
|
48
50
|
} from './shared';
|
|
49
51
|
|
|
52
|
+
/** The one status the buyer status table calls paid and still moving: the buyer paid, the shop is restocking. */
|
|
53
|
+
export const WAITING_ORDER_STATUS = 'AWAITING_STOCK' satisfies BuyerInvoiceStatus;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Nothing to download yet. When a paid order waits for stock, the empty state
|
|
57
|
+
* names it (Design Batch 9, C7), asked of the producer by status, never picked
|
|
58
|
+
* out of a page in hand. Without one, or while that read has not answered, the
|
|
59
|
+
* general sentence stands; it is true either way.
|
|
60
|
+
*/
|
|
61
|
+
function EmptyDownloads() {
|
|
62
|
+
const { state } = useAccountResource('downloads:waiting-order', () =>
|
|
63
|
+
callCustomerApi((sdk) => sdk.orders({ page: 1, limit: 1, status: WAITING_ORDER_STATUS })));
|
|
64
|
+
const waiting = state.status === 'ready' ? state.data.invoices[0] : undefined;
|
|
65
|
+
const item = waiting?.lineItems[0]?.productTitle;
|
|
66
|
+
|
|
67
|
+
if (waiting && item) {
|
|
68
|
+
return (
|
|
69
|
+
<AccountEmpty
|
|
70
|
+
title={t('downloads.emptyTitle')}
|
|
71
|
+
body={t('downloads.waitingOrder', { id: waiting.uniqid, item })}
|
|
72
|
+
action={(
|
|
73
|
+
<Link to={accountOrderHref(waiting.uniqid)} className="sx-account-button sxa:card-button sxa:card-button--add">
|
|
74
|
+
{t('downloads.openOrder')}
|
|
75
|
+
</Link>
|
|
76
|
+
)}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<AccountEmpty
|
|
83
|
+
title={t('downloads.emptyTitle')}
|
|
84
|
+
body={t('downloads.emptyBody')}
|
|
85
|
+
action={(
|
|
86
|
+
<Link to="/all-products" className="sx-account-button sxa:card-button sxa:card-button--add">
|
|
87
|
+
{t('common.browseStore')}
|
|
88
|
+
</Link>
|
|
89
|
+
)}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
50
94
|
export function AccountDownloads() {
|
|
51
95
|
const { state, reload } = useAccountDashboard();
|
|
52
96
|
const [notice, setNotice] = useState<string | null>(null);
|
|
@@ -79,19 +123,7 @@ export function AccountDownloads() {
|
|
|
79
123
|
|
|
80
124
|
const { groups } = grouped;
|
|
81
125
|
|
|
82
|
-
if (isEmptyDownloadGroups(groups))
|
|
83
|
-
return (
|
|
84
|
-
<AccountEmpty
|
|
85
|
-
title={t('downloads.emptyTitle')}
|
|
86
|
-
body={t('downloads.emptyBody')}
|
|
87
|
-
action={(
|
|
88
|
-
<Link to="/all-products" className="sx-account-button sxa:card-button sxa:card-button--add">
|
|
89
|
-
{t('common.browseStore')}
|
|
90
|
-
</Link>
|
|
91
|
-
)}
|
|
92
|
-
/>
|
|
93
|
-
);
|
|
94
|
-
}
|
|
126
|
+
if (isEmptyDownloadGroups(groups)) return <EmptyDownloads />;
|
|
95
127
|
|
|
96
128
|
const resetHardwareId = async (uniqid: string) => {
|
|
97
129
|
setResettingUniqid(uniqid);
|
|
@@ -93,11 +93,25 @@
|
|
|
93
93
|
"PAID": "مدفوع",
|
|
94
94
|
"FAILED": "فشل"
|
|
95
95
|
},
|
|
96
|
+
"invoiceStatus": {
|
|
97
|
+
"awaitingPayment": "بانتظار الدفع",
|
|
98
|
+
"processing": "جارٍ تأكيد الدفع",
|
|
99
|
+
"underpaid": "الدفع غير مكتمل",
|
|
100
|
+
"awaitingStock": "بانتظار توفر المخزون",
|
|
101
|
+
"completed": "مكتمل",
|
|
102
|
+
"partialRefund": "مُسترد جزئيًا",
|
|
103
|
+
"refunded": "مُسترَد",
|
|
104
|
+
"disputed": "متنازع عليه",
|
|
105
|
+
"expired": "منتهي الصلاحية",
|
|
106
|
+
"voided": "ألغاها المتجر",
|
|
107
|
+
"paid": "مدفوع"
|
|
108
|
+
},
|
|
96
109
|
"delivery": {
|
|
97
110
|
"DELIVERED": "تم التسليم",
|
|
98
111
|
"AWAITING_FULFILLMENT": "بانتظار التنفيذ",
|
|
99
112
|
"PENDING": "قيد الانتظار",
|
|
100
|
-
"FAILED": "فشل التسليم"
|
|
113
|
+
"FAILED": "فشل التسليم",
|
|
114
|
+
"WAITING": "في الانتظار"
|
|
101
115
|
},
|
|
102
116
|
"overview": {
|
|
103
117
|
"loading": "جارٍ تحميل حسابك",
|
|
@@ -140,12 +154,6 @@
|
|
|
140
154
|
"searchPlaceholder": "البحث برقم الفاتورة",
|
|
141
155
|
"status": "الحالة",
|
|
142
156
|
"statusAll": "كل الحالات",
|
|
143
|
-
"statusCompleted": "مكتمل",
|
|
144
|
-
"statusPending": "قيد الانتظار",
|
|
145
|
-
"statusProcessing": "قيد المعالجة",
|
|
146
|
-
"statusRefunded": "مُسترَد",
|
|
147
|
-
"statusDisputed": "متنازع عليه",
|
|
148
|
-
"statusExpired": "منتهي الصلاحية",
|
|
149
157
|
"emptyFiltered": "لا توجد طلبات مطابقة. جرّب رقم فاتورة أو حالة أخرى.",
|
|
150
158
|
"warrantyTitle": "الضمان",
|
|
151
159
|
"warrantyCoveredUntil": "مغطى حتى {date}",
|
|
@@ -164,7 +172,6 @@
|
|
|
164
172
|
"warrantyClaimTooLong": "لا يمكن أن يتجاوز طلبك {max} حرف.",
|
|
165
173
|
"invoicePdf": "تنزيل الفاتورة (PDF)",
|
|
166
174
|
"invoicePdfLoading": "جارٍ التحضير…",
|
|
167
|
-
"invoicePdfFailed": "تعذّر تنزيل هذه الفاتورة. يُرجى المحاولة مرة أخرى.",
|
|
168
175
|
"reviewTitle": "تقييمك",
|
|
169
176
|
"reviewPrompt": "ما رأيك في {product}؟",
|
|
170
177
|
"reviewPromptGeneric": "ما رأيك في عملية الشراء؟",
|
|
@@ -174,7 +181,19 @@
|
|
|
174
181
|
"reviewEdit": "تعديل التقييم",
|
|
175
182
|
"reviewWrite": "اكتب تقييمًا",
|
|
176
183
|
"reviewAutomated": "تم نشر تقييم إيجابي تلقائي لهذا الطلب. يمكنك استبداله بتقييمك.",
|
|
177
|
-
"reviewFallbackLabel": "تقييم"
|
|
184
|
+
"reviewFallbackLabel": "تقييم",
|
|
185
|
+
"nothingToDownload": "تم التسليم {date}. سلّمك البائع هذا مباشرةً، لذلك لا يوجد ما يمكن تنزيله.",
|
|
186
|
+
"detailError": "تعذّر تحميل الطلب {id}",
|
|
187
|
+
"noReceiptUnpaid": "لم يُدفع هذا الطلب قط، لذلك لا يوجد له إيصال.",
|
|
188
|
+
"noReceiptVoided": "ألغى المتجر هذا الطلب، لذلك لا يوجد له إيصال.",
|
|
189
|
+
"pdfSignedOut": "تم تسجيل خروجك. سجّل الدخول مرة أخرى لتنزيل الإيصال.",
|
|
190
|
+
"pdfFailed": "تعذّر إنشاء الإيصال. حاول مرة أخرى بعد دقيقة.",
|
|
191
|
+
"waitingStock": {
|
|
192
|
+
"title": "لقد دفعت. البائع يعيد تعبئة المخزون.",
|
|
193
|
+
"body": "سيظهر «{item}» هنا فور تسليمه.",
|
|
194
|
+
"bodyOrder": "سيظهر طلبك هنا فور تسليمه."
|
|
195
|
+
},
|
|
196
|
+
"paidOn": "تم الدفع {date}"
|
|
178
197
|
},
|
|
179
198
|
"downloads": {
|
|
180
199
|
"loading": "جارٍ تحميل تنزيلاتك",
|
|
@@ -216,7 +235,9 @@
|
|
|
216
235
|
"accessRequestFailed": "تعذّر إرسال طلبك. يُرجى المحاولة مرة أخرى.",
|
|
217
236
|
"accessRequestTitle": "الوصول إلى {file}",
|
|
218
237
|
"accessRequestMessage": "أرغب في تنزيل {file} ({product}) من هذا الطلب مرة أخرى.",
|
|
219
|
-
"accessRequestReason": "تعرض صفحة الحساب: {reason}."
|
|
238
|
+
"accessRequestReason": "تعرض صفحة الحساب: {reason}.",
|
|
239
|
+
"waitingOrder": "الطلب {id} مدفوع وبانتظار توفر المخزون. سيظهر «{item}» هنا بمجرد تسليمه.",
|
|
240
|
+
"openOrder": "فتح الطلب"
|
|
220
241
|
},
|
|
221
242
|
"subscriptions": {
|
|
222
243
|
"loading": "جارٍ تحميل اشتراكاتك",
|
|
@@ -227,6 +248,9 @@
|
|
|
227
248
|
"renews": "يتجدّد في {date}",
|
|
228
249
|
"ends": "ينتهي في {date}",
|
|
229
250
|
"canceled": "أُلغي في {date}",
|
|
251
|
+
"endedOn": "انتهى في {date}",
|
|
252
|
+
"endedHoldRanOut": "انتهت فترة الإيقاف دون دفع. يمكنك الاشتراك مجددًا من المتجر.",
|
|
253
|
+
"endedShopEnded": "لم يُدفع التجديد وأنهى المتجر الاشتراك. يمكنك الاشتراك مجددًا من المتجر.",
|
|
230
254
|
"startedOn": "بدأ في {date}",
|
|
231
255
|
"billingDayOne": "فوترة يومية",
|
|
232
256
|
"billingDayOther": "فوترة كل {count} أيام",
|
|
@@ -243,7 +267,6 @@
|
|
|
243
267
|
"noPaymentMethod": "لا توجد طريقة محفوظة",
|
|
244
268
|
"pause": "إيقاف مؤقت",
|
|
245
269
|
"pausing": "جارٍ الإيقاف…",
|
|
246
|
-
"resume": "استئناف",
|
|
247
270
|
"resuming": "جارٍ الاستئناف…",
|
|
248
271
|
"cancel": "إلغاء الاشتراك",
|
|
249
272
|
"cancelling": "جارٍ الإلغاء…",
|
|
@@ -252,7 +275,6 @@
|
|
|
252
275
|
"confirmCancelAtPeriodEnd": "يبقى وصولك حتى {date}. لن تُحصَّل أي مبالغ بعد ذلك.",
|
|
253
276
|
"confirmCancelNow": "ينتهي الوصول فور الإلغاء.",
|
|
254
277
|
"actionFailed": "لم تتم العملية. يُرجى المحاولة مرة أخرى.",
|
|
255
|
-
"paused": "هذا الاشتراك متوقف مؤقتًا.",
|
|
256
278
|
"resumed": "هذا الاشتراك يعمل من جديد.",
|
|
257
279
|
"cancelled": "تم إلغاء هذا الاشتراك.",
|
|
258
280
|
"historyTitle": "سجل الفواتير",
|
|
@@ -260,11 +282,75 @@
|
|
|
260
282
|
"historyEmptyTitle": "لا يوجد سجل فواتير بعد",
|
|
261
283
|
"historyEmptyBody": "تظهر فواتير التجديد هنا بعد الدورة الأولى.",
|
|
262
284
|
"trial": "تنتهي الفترة التجريبية في {date}",
|
|
285
|
+
"statusOnHold": "معلّق",
|
|
286
|
+
"statusPaymentDue": "الدفع مستحق",
|
|
287
|
+
"holdNote": "تم إيقاف وصولك مؤقتًا. ادفع {amount} قبل {date} وسيعود فورًا، مع {interval} من يوم الدفع.",
|
|
288
|
+
"dueNote": "تجديدك بتاريخ {renewal} غير مدفوع. ادفع قبل {date} للحفاظ على وصولك.",
|
|
289
|
+
"readyNote": "تجديدك بتاريخ {renewal} جاهز. ادفع في أي وقت قبل ذلك؛ {interval} في {renewal} كالمعتاد.",
|
|
290
|
+
"holdIntervalDay": "يوم جديد يبدأ",
|
|
291
|
+
"holdIntervalWeek": "أسبوع جديد يبدأ",
|
|
292
|
+
"holdIntervalMonth": "شهر جديد يبدأ",
|
|
293
|
+
"holdIntervalYear": "سنة جديدة تبدأ",
|
|
294
|
+
"holdIntervalOther": "فترة فوترة جديدة تبدأ",
|
|
295
|
+
"readyIntervalDay": "سيبدأ اليوم التالي",
|
|
296
|
+
"readyIntervalWeek": "سيبدأ الأسبوع التالي",
|
|
297
|
+
"readyIntervalMonth": "سيبدأ الشهر التالي",
|
|
298
|
+
"readyIntervalYear": "ستبدأ السنة التالية",
|
|
299
|
+
"readyIntervalOther": "ستبدأ فترة الفوترة التالية",
|
|
300
|
+
"payAndResume": "ادفع {amount} واستأنف",
|
|
301
|
+
"payAmount": "ادفع {amount} الآن",
|
|
302
|
+
"pauses": "يتوقف مؤقتًا في {date}",
|
|
303
|
+
"payBy": "ادفع قبل {date}",
|
|
304
|
+
"resumes": "يُستأنف في {date}",
|
|
305
|
+
"pausedOpenEnded": "متوقف مؤقتًا حتى تستأنفه",
|
|
306
|
+
"pauseTitle": "إيقاف {plan} مؤقتًا؟",
|
|
307
|
+
"pauseStartsAfter": "تتوقف خطتك بعد {date}، أي في نهاية الفترة التي دفعت مقابلها.",
|
|
308
|
+
"pauseHowLong": "المدة",
|
|
309
|
+
"pauseReasonLabel": "لماذا توقفه مؤقتًا؟",
|
|
310
|
+
"pauseOptional": "اختياري",
|
|
311
|
+
"pauseReasonNone": "اختر سببًا",
|
|
312
|
+
"pauseReasonTakingABreak": "آخذ استراحة",
|
|
313
|
+
"pauseReasonTooExpensive": "مكلف جدًا حاليًا",
|
|
314
|
+
"pauseReasonNotUsing": "لا أستخدمه بما يكفي",
|
|
315
|
+
"pauseReasonNotWorking": "هناك شيء لا يعمل",
|
|
316
|
+
"pauseReasonOther": "سبب آخر",
|
|
317
|
+
"pauseLength7": "أسبوع واحد",
|
|
318
|
+
"pauseLength14": "أسبوعان",
|
|
319
|
+
"pauseLength30": "شهر واحد",
|
|
320
|
+
"pauseLength60": "شهران",
|
|
321
|
+
"pauseLength90": "3 أشهر",
|
|
322
|
+
"pauseSummary": "لا دفع من {start} إلى {end}. يُستأنف تلقائيًا في {end}.",
|
|
323
|
+
"pauseResumeAnyTime": "يمكنك الاستئناف قبل ذلك في أي وقت.",
|
|
324
|
+
"pauseLeftNone": "هذا آخر إيقاف مؤقت لك خلال الأشهر الـ12 القادمة.",
|
|
325
|
+
"pauseLeftOne": "يمكنك الإيقاف المؤقت مرة واحدة أخرى خلال الأشهر الـ12 القادمة.",
|
|
326
|
+
"pauseLeftMany": "عدد مرات الإيقاف المؤقت المتبقية خلال الأشهر الـ12 القادمة: {count}.",
|
|
327
|
+
"pauseConfirm": "إيقاف مؤقت حتى {date}",
|
|
328
|
+
"pauseKeep": "الاحتفاظ بالاشتراك",
|
|
329
|
+
"pauseError": "تعذّر إيقاف اشتراكك مؤقتًا. لم يتغير شيء. حاول مرة أخرى بعد قليل.",
|
|
330
|
+
"pausedUntil": "متوقف مؤقتًا حتى {date}",
|
|
331
|
+
"pausedBody": "لا دفع حتى ذلك الحين.",
|
|
332
|
+
"pauseAccessRole": "يُزال دورك {roles} أثناء الإيقاف المؤقت.",
|
|
333
|
+
"pauseAccessRoles": "تُزال أدوارك {roles} أثناء الإيقاف المؤقت.",
|
|
334
|
+
"pauseAccess": "يُزال وصولك أثناء الإيقاف المؤقت.",
|
|
335
|
+
"pausedPayToday": "إذا استأنفت الآن فستدفع {price} اليوم مقابل فترة فوترة جديدة.",
|
|
336
|
+
"pauseScheduled": "يتوقف مؤقتًا بعد {date}",
|
|
337
|
+
"pauseScheduledBody": "يبقى وصولك حتى ذلك الحين. يُستأنف في {date}.",
|
|
338
|
+
"pauseScheduledBodyOpenEnded": "يبقى وصولك حتى ذلك الحين.",
|
|
339
|
+
"resumeNow": "استئناف الآن",
|
|
340
|
+
"dontPause": "عدم الإيقاف المؤقت",
|
|
341
|
+
"pauseLimit": "استخدمت مرات الإيقاف المؤقت لهذا العام (العدد: {count}). يمكنك الإيقاف المؤقت مجددًا بدءًا من {date}.",
|
|
342
|
+
"pauseLimitOne": "استخدمت الإيقاف المؤقت لهذا العام. يمكنك الإيقاف المؤقت مجددًا بدءًا من {date}.",
|
|
343
|
+
"pauseSavedNotice": "تم ضبط الإيقاف المؤقت.",
|
|
344
|
+
"pauseCalledOff": "لن يتوقف اشتراكك مؤقتًا.",
|
|
263
345
|
"renewalDue": "توجد دفعة تجديد مستحقة لهذا الاشتراك.",
|
|
264
346
|
"payNow": "ادفع الآن",
|
|
265
347
|
"updateCard": "تحديث البطاقة",
|
|
266
348
|
"pastDueCardTitle": "فشلت دفعتك الأخيرة",
|
|
267
|
-
"pastDueCardBody": "حدّث بطاقتك للحفاظ على هذا الاشتراك. تُفتح صفحة Shoppex آمنة."
|
|
349
|
+
"pastDueCardBody": "حدّث بطاقتك للحفاظ على هذا الاشتراك. تُفتح صفحة Shoppex آمنة.",
|
|
350
|
+
"historyFirstOrder": "الطلب الأول",
|
|
351
|
+
"historyRenewal": "تجديد",
|
|
352
|
+
"historyPlanChange": "تغيير الخطة، من {from} إلى {to}",
|
|
353
|
+
"historyDays": "عن {days} من {total} يومًا"
|
|
268
354
|
},
|
|
269
355
|
"favorites": {
|
|
270
356
|
"loading": "جارٍ تحميل مفضلتك",
|
|
@@ -525,4 +611,3 @@
|
|
|
525
611
|
"neverUsed": "لم يُستخدم"
|
|
526
612
|
}
|
|
527
613
|
}
|
|
528
|
-
|