@tallyui/components 3.0.0-next.1 → 3.0.0-next.2

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/dist/index.d.ts CHANGED
@@ -737,7 +737,7 @@ interface CustomerPickerProps {
737
737
  create?: (input: CustomerInput) => Promise<Customer>;
738
738
  selected: Customer | null;
739
739
  onSelect: (customer: Customer | null) => void;
740
- /** Called for errors the picker can't explain to the cashier, including ConnectorUnauthorizedError, which means the app should sign in again. */
740
+ /** Called for errors the picker can't explain to the cashier, including `ConnectorUnauthorizedError`: sign in again only when its `code` is `'unauthorized'` (401). `'forbidden'` (403) means signed in but not allowed; never sign out for it. */
741
741
  onError?: (error: unknown) => void;
742
742
  /** false disables search and create (online only in v1). */
743
743
  online: boolean;
package/dist/index.js CHANGED
@@ -1734,7 +1734,7 @@ var STATUS_STYLES = {
1734
1734
  };
1735
1735
  function formatAsOf(iso) {
1736
1736
  const date = new Date(iso);
1737
- return date.toDateString() === (/* @__PURE__ */ new Date()).toDateString() ? date.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit" }) : date.toLocaleDateString(void 0, { day: "numeric", month: "short" });
1737
+ return date.toDateString() === (/* @__PURE__ */ new Date()).toDateString() ? date.toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" }) : date.toLocaleDateString(void 0, { day: "numeric", month: "short" });
1738
1738
  }
1739
1739
  function ProductStockBadge({ doc, showQuantity = false, showAsOf = true, className, ...props }) {
1740
1740
  const { status, quantity, asOf } = useProductStock(doc);
@@ -4229,7 +4229,7 @@ var STOCK_LABEL = {
4229
4229
  unknown: "Unknown"
4230
4230
  };
4231
4231
  function formatStockSyncTime(time, locale, hour12, now = /* @__PURE__ */ new Date()) {
4232
- const timeLabel = time.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit", hour12 });
4232
+ const timeLabel = time.toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit", hour12 });
4233
4233
  if (time.toDateString() === now.toDateString()) return timeLabel;
4234
4234
  const dateLabel = time.toLocaleDateString(locale, { day: "numeric", month: "short" });
4235
4235
  return `${dateLabel}, ${timeLabel}`;
@@ -4428,6 +4428,14 @@ function Receipt({
4428
4428
  // src/sale/sync-status.tsx
4429
4429
  import { useEffect as useEffect7, useRef as useRef6, useState as useState18 } from "react";
4430
4430
  import { AccessibilityInfo as AccessibilityInfo2, Platform as Platform3, Text as Text38, View as View59 } from "react-native";
4431
+
4432
+ // src/sale/since-text.ts
4433
+ function sinceText(clock) {
4434
+ const time = (t) => new Date(t).toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
4435
+ return clock.firstFailedAt === void 0 ? "about " + time(clock.since) : time(clock.firstFailedAt);
4436
+ }
4437
+
4438
+ // src/sale/sync-status.tsx
4431
4439
  import { Fragment as Fragment7, jsx as jsx93, jsxs as jsxs52 } from "react/jsx-runtime";
4432
4440
  var PULL_NOTICE_TEXT = {
4433
4441
  unauthorized: {
@@ -4467,7 +4475,7 @@ var BACKEND_MISSING_DETAIL = "This till couldn't find {pluginName} on the online
4467
4475
  var REFUSED = (n, waiting) => n === 1 ? `1 sale needs attention${waiting} \xB7 The online store refused it. Ask the store owner to look at the till's sync log.` : `${n} sales need attention${waiting} \xB7 The online store refused them. Ask the store owner to look at the till's sync log.`;
4468
4476
  var REFUSED_DETAIL = "Refused sales stay on this till under Needs attention, each with what to do next.";
4469
4477
  var SALES_REFUSED = "The online store refused the last send. This till will try again with the next sale, or when the app is reopened.";
4470
- var TILL_UPDATES_REFUSED = "The online store refused the last send. This till will try again with the next till update.";
4478
+ var TILL_UPDATES_REFUSED = "The online store refused the last send. This till will try again with the next till update, or when the app is reopened.";
4471
4479
  var count = (n, noun) => `${n} ${noun}${n === 1 ? "" : "s"}`;
4472
4480
  function useAnnounceOnIos(...texts) {
4473
4481
  const announced = useRef6(texts);
@@ -4494,8 +4502,7 @@ function SyncStatus({ state, registerState, pluginName = "the POS plugin", pullN
4494
4502
  ].filter(Boolean).join(" and ") + " waiting to sync";
4495
4503
  const { stuck } = state;
4496
4504
  const backendMissing = state.backendMissing ?? registerState?.backendMissing;
4497
- const at = (time) => new Date(time).toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" });
4498
- const backendMissingText = state.pending === 0 && updates > 0 ? registerState?.stuck ? TILL_UPDATES_MISSING_SINCE.replace("{time}", () => at(registerState.stuck.since)) : TILL_UPDATES_MISSING : stuck ? BACKEND_MISSING_SINCE.replace("{time}", () => at(stuck.since)) : BACKEND_MISSING;
4505
+ const backendMissingText = state.pending === 0 && updates > 0 ? registerState?.stuck ? TILL_UPDATES_MISSING_SINCE.replace("{time}", () => sinceText(registerState.stuck)) : TILL_UPDATES_MISSING : stuck ? BACKEND_MISSING_SINCE.replace("{time}", () => sinceText(stuck)) : BACKEND_MISSING;
4499
4506
  const upToDate = state.pending === 0 && updates === 0;
4500
4507
  const sentence = outbox.refused ? ` \xB7 ${outbox === state ? SALES_REFUSED : TILL_UPDATES_REFUSED}` : backendMissing || (state.pending === 0 && updates > 0 ? registerState?.stuck : stuck) ? ` \xB7 ${backendMissingText}` : "";
4501
4508
  const refused = state.rejected ? REFUSED(state.rejected, upToDate ? "" : `, ${updates ? label : `${state.pending} waiting to sync`}`) : "";
@@ -4573,7 +4580,7 @@ function OrdersList({ orders, onRetry, formatDate = defaultFormatDate2, footer,
4573
4580
  " \xB7 ",
4574
4581
  STATUS_LABEL[order.syncStatus]
4575
4582
  ] }),
4576
- stuckEntry ? /* @__PURE__ */ jsx94(Text39, { className: "text-destructive", children: `Hasn't reached the online store since ${new Date(stuckEntry.since).toLocaleTimeString(void 0, { hour: "numeric", minute: "2-digit" })}.` }) : null,
4583
+ stuckEntry ? /* @__PURE__ */ jsx94(Text39, { className: "text-destructive", children: `Hasn't reached the online store since ${sinceText(stuckEntry)}.` }) : null,
4577
4584
  order.syncStatus === "rejected" ? /* @__PURE__ */ jsx94(Text39, { className: "text-destructive", children: refusalSentence(order.error?.code) }) : null,
4578
4585
  order.lateSessionId !== void 0 ? /* @__PURE__ */ jsx94(Text39, { className: "text-foreground", children: "Taken after the register closed. It is not in that register's closure." }) : null,
4579
4586
  order.localWarnings?.map((warning, index) => /* @__PURE__ */ jsx94(Text39, { className: "text-foreground", children: warning.code === "customer_omitted" ? `The customer's ${warning.field} couldn't be sent to the store, so the order isn't linked to them.` : "The terminal's payment reference couldn't be kept; the payment is recorded without it." }, index)),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tallyui/components",
3
- "version": "3.0.0-next.1",
3
+ "version": "3.0.0-next.2",
4
4
  "type": "module",
5
5
  "description": "Universal POS UI components for Tally UI",
6
6
  "main": "./dist/index.js",
@@ -27,22 +27,22 @@
27
27
  "directory": "packages/components"
28
28
  },
29
29
  "peerDependencies": {
30
- "@tallyui/core": "^3.0.0-next.1",
31
- "@tallyui/primitives": "^3.0.0-next.1",
32
- "@tallyui/theme": "^3.0.0-next.1",
30
+ "@tallyui/core": "^3.0.0-next.2",
31
+ "@tallyui/primitives": "^3.0.0-next.2",
32
+ "@tallyui/theme": "^3.0.0-next.2",
33
33
  "react": ">=18",
34
34
  "react-native": ">=0.74",
35
35
  "react-native-svg": ">=15"
36
36
  },
37
37
  "dependencies": {
38
38
  "class-variance-authority": "^0.7.1",
39
- "@tallyui/pos": "3.0.0-next.1"
39
+ "@tallyui/pos": "3.0.0-next.2"
40
40
  },
41
41
  "devDependencies": {
42
- "@tallyui/connector-medusa": "3.0.0-next.1",
43
- "@tallyui/connector-vendure": "3.0.0-next.1",
44
- "@tallyui/connector-woocommerce": "3.0.0-next.1",
45
- "@tallyui/mock-api": "3.0.0-next.1",
42
+ "@tallyui/connector-medusa": "3.0.0-next.2",
43
+ "@tallyui/connector-vendure": "3.0.0-next.2",
44
+ "@tallyui/connector-woocommerce": "3.0.0-next.2",
45
+ "@tallyui/mock-api": "3.0.0-next.2",
46
46
  "@types/react": ">=18",
47
47
  "react": "^19.2.4",
48
48
  "react-native-svg": "15.12.1",
@@ -13,7 +13,7 @@ export interface CustomerPickerProps {
13
13
  create?: (input: CustomerInput) => Promise<Customer>;
14
14
  selected: Customer | null;
15
15
  onSelect: (customer: Customer | null) => void;
16
- /** Called for errors the picker can't explain to the cashier, including ConnectorUnauthorizedError, which means the app should sign in again. */
16
+ /** Called for errors the picker can't explain to the cashier, including `ConnectorUnauthorizedError`: sign in again only when its `code` is `'unauthorized'` (401). `'forbidden'` (403) means signed in but not allowed; never sign out for it. */
17
17
  onError?: (error: unknown) => void;
18
18
  /** false disables search and create (online only in v1). */
19
19
  online: boolean;
@@ -49,7 +49,7 @@ const STATUS_STYLES: Record<StockStatus, { badge: string; dot: string; text: str
49
49
  function formatAsOf(iso: string): string {
50
50
  const date = new Date(iso);
51
51
  return date.toDateString() === new Date().toDateString()
52
- ? date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
52
+ ? date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })
53
53
  : date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
54
54
  }
55
55
 
@@ -16,7 +16,7 @@ const STOCK_LABEL = {
16
16
  // function) so the formatting is testable without the device; callers default them to the
17
17
  // device's own settings and the current moment.
18
18
  export function formatStockSyncTime(time: Date, locale?: string, hour12?: boolean, now: Date = new Date()): string {
19
- const timeLabel = time.toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12 });
19
+ const timeLabel = time.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit', hour12 });
20
20
  // "Today" is decided in the device's own timezone: toDateString() reads the Date in local time,
21
21
  // the same timezone the device clock and toLocaleTimeString above already use.
22
22
  if (time.toDateString() === now.toDateString()) return timeLabel;
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState, type ReactNode } from 'react';
2
2
  import { Pressable, ScrollView, Text, View } from 'react-native';
3
3
  import { formatMoney, knownWarnings } from '@tallyui/core';
4
4
  import { needsAttention, type OutboxState, type PosOrder } from '@tallyui/pos';
5
+ import { sinceText } from './since-text';
5
6
 
6
7
  const STATUS_LABEL = { pending: 'Waiting to sync', applied: 'Synced', rejected: 'Not accepted' };
7
8
  /** `figures_mismatch`'s labels; a field a newer store sends that isn't here is shown by its raw name. */
@@ -61,7 +62,7 @@ export function OrdersList({ orders, onRetry, formatDate = defaultFormatDate, fo
61
62
  <Text className="text-muted-foreground">{formatDate(order.createdAt)} · {formatMoney({ amount: order.totalMinor, currency: order.currency })} · {STATUS_LABEL[order.syncStatus]}</Text>
62
63
  {stuckEntry ? <Text className="text-destructive">
63
64
  {/* The same words for any reason, with no reason code; the hour as the status line has it (numeric, #245). */}
64
- {`Hasn't reached the online store since ${new Date(stuckEntry.since).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' })}.`}
65
+ {`Hasn't reached the online store since ${sinceText(stuckEntry)}.`}
65
66
  </Text> : null}
66
67
  {/* The code's sentence alone: the cashier sees no error code and never the store's message. */}
67
68
  {order.syncStatus === 'rejected' ? <Text className="text-destructive">{refusalSentence(order.error?.code)}</Text> : null}
@@ -0,0 +1,7 @@
1
+ // The time a stuck line names: the real first failure when this run has one, else "about" the virtual since (after a restart
2
+ // firstFailedAt is gone; see OutboxState.stuck).
3
+ export function sinceText(clock: { since: number; firstFailedAt?: number }): string {
4
+ // The device's own 12/24-hour format, with no forced leading zero on the hour.
5
+ const time = (t: number) => new Date(t).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
6
+ return clock.firstFailedAt === undefined ? 'about ' + time(clock.since) : time(clock.firstFailedAt);
7
+ }
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from 'react';
2
2
  import { AccessibilityInfo, Platform, Text, View } from 'react-native';
3
3
  import type { SyncNotice } from '@tallyui/core';
4
4
  import type { OutboxState } from '@tallyui/pos';
5
+ import { sinceText } from './since-text';
5
6
 
6
7
  /**
7
8
  * What the cashier reads when the product pull stops, keyed by the notice's code; the code itself is never shown.
@@ -47,7 +48,7 @@ const PULL_NOTICE_FALLBACK: PullNoticeText = {
47
48
 
48
49
  // The line the cashier reads while the store keeps answering 404 (OutboxState.backendMissing) and no order is stuck.
49
50
  const BACKEND_MISSING = "Sales aren't reaching the online store. Keep selling: they're saved on this till and will send by themselves.";
50
- // The same line once an order is stuck, with the store missing or not; {time} becomes stuck.since.
51
+ // The same line once an order is stuck, with the store missing or not; {time} is the real first failure or about the virtual since.
51
52
  const BACKEND_MISSING_SINCE = "Sales haven't reached the online store since {time}. Keep selling: they're saved on this till and will send by themselves.";
52
53
  // The line instead when no sale waits but till updates do, and its "since" variant once registerState.stuck is set.
53
54
  const TILL_UPDATES_MISSING = "Till updates aren't reaching the online store. Keep selling: they're saved on this till and will send by themselves.";
@@ -66,10 +67,10 @@ const REFUSED = (n: number, waiting: string) => n === 1
66
67
  const REFUSED_DETAIL = 'Refused sales stay on this till under Needs attention, each with what to do next.';
67
68
  // The sentence after the waiting count once the store refused a whole batch (OutboxState.refused) from the outbox shown, when
68
69
  // no command carries a code. Neither outbox sets a timer then: a new pending command flushes its outbox. For sales,
69
- // useOrderOutbox also calls start() when the store opens, which flushes; no app starts the register outbox at launch today,
70
- // so the till-updates sentence names only the next till update (#269).
70
+ // useOrderOutbox calls start() when its store opens and useRegisterOutbox when its collection opens, so both flush
71
+ // on reopen and both sentences name a reopen.
71
72
  const SALES_REFUSED = 'The online store refused the last send. This till will try again with the next sale, or when the app is reopened.';
72
- const TILL_UPDATES_REFUSED = 'The online store refused the last send. This till will try again with the next till update.';
73
+ const TILL_UPDATES_REFUSED = 'The online store refused the last send. This till will try again with the next till update, or when the app is reopened.';
73
74
 
74
75
  const count = (n: number, noun: string) => `${n} ${noun}${n === 1 ? '' : 's'}`;
75
76
  // iOS has no live region (accessibilityLiveRegion is Android-only), so there each text is announced when it changes: never on
@@ -103,11 +104,9 @@ export function SyncStatus({ state, registerState, pluginName = 'the POS plugin'
103
104
  updates && count(updates, 'till update')].filter(Boolean).join(' and ') + ' waiting to sync';
104
105
  const { stuck } = state;
105
106
  const backendMissing = state.backendMissing ?? registerState?.backendMissing;
106
- // The device's own 12/24-hour format, with no forced leading zero on the hour.
107
- const at = (time: number) => new Date(time).toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
108
107
  const backendMissingText = state.pending === 0 && updates > 0 ? (registerState?.stuck
109
- ? TILL_UPDATES_MISSING_SINCE.replace('{time}', () => at(registerState.stuck!.since)) : TILL_UPDATES_MISSING)
110
- : stuck ? BACKEND_MISSING_SINCE.replace('{time}', () => at(stuck.since)) : BACKEND_MISSING;
108
+ ? TILL_UPDATES_MISSING_SINCE.replace('{time}', () => sinceText(registerState.stuck!)) : TILL_UPDATES_MISSING)
109
+ : stuck ? BACKEND_MISSING_SINCE.replace('{time}', () => sinceText(stuck)) : BACKEND_MISSING;
111
110
  const upToDate = state.pending === 0 && updates === 0;
112
111
  // The sentence shows with the store missing, or once the waiting sales (with none waiting, the till updates) are stuck, then as
113
112
  // its "since" variant: the same words for any reason, and no reason code.