The subscribe/disable flow awaited several browser APIs that on certain
devices — most reliably iOS Safari in a non-installed PWA — return a
Promise that never resolves or rejects (requestPermission, serviceWorker
.ready, pushManager.subscribe). When any of them hung, the try/finally
never ran, so `busy` stayed true forever: the dashboard nudge button
stuck at "..." and the settings toggle refused to flip.
Every awaited step is now wrapped in withTimeout(15s). Failures surface
as toasts instead of silent hangs (subscribe success, permission denied,
generic error). iOS Safari without the site installed to home screen —
where Web Push physically cannot work — short-circuits with an
add-to-home-screen instruction before we ever try, so we don't burn a
timeout waiting for a doomed subscribe().
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- MessageInput: paperclip button (team-only, controlled by teamUserIds
from ThreadDetail) uploads through the media service and hands the URL
to the WebSocket; Enter now inserts a newline — sending is button-only
since accidental Enter-sends were noisy for staff replies.
- MessageBubble: renders image messages inline (tap to open), shows the
coworker's username at the top of team-side bubbles so staff can tell
who typed what.
- MessageList / ThreadChatPage: any message from a store team member
(owner or active staff) renders on the "our" side of the bubble even
for a viewer who isn't the sender, using team_user_ids from
ThreadDetail. Delete option stays gated to the actual sender.
- ThreadsPage: inbox now shows smart-relative timestamps
(today HH:MM / دیروز HH:MM / N روز پیش / Persian date), plus a
reply-state icon per row (red bell = customer waiting, blue double-check
= you replied and they've seen it, nothing = you replied not yet seen).
Image-only last messages preview as "📷 تصویر".
- Push notifications: on mount we re-POST the current PushSubscription so
a browser-rotated sub or a backend row deleted after a 410 auto-heals.
- useWebSocket: SendMessagePayload and NewMessageEvent carry image_url so
the server relay can round-trip images.
- API types: MessageList.imageUrl, LastMessage.imageUrl,
ThreadList.lastMessageFromTeam/otherPartySeenLast, ThreadDetail.teamUserIds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- app/hooks/useStoreOwnership.ts: the old redirect logic sent owners to /
the moment useMyStores briefly resolved to []. That empty array came from
useMyStores's dead `if (!token) return []` inside the queryFn, so any
transient auth blip left the store page thinking the user has no stores.
Now compares usernames case-insensitively, only redirects while the query
is settled (!isLoading && !isFetching && !isError), and never bounces to
home on empty — worst case the seller sees an empty dashboard rather than
a silent kick-out.
- app/requestHandler/use-seller-hooks.ts: removed the unreachable
`if (!token) return []` in useMyStores and added the token to the
queryKey so a login switch stops serving the previous user's cached list.
- app/routes/store.$storeId._index.tsx: mount StorePushPromptCard at the
top of the store dashboard.
- app/routes/store.$storeId.tsx: sidebar "پیامها" badge reads
badges.sellerChat[storeId] (per-store) instead of the buyer-side
badges.chat, matching the badge-serializer split.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- app/components/MyLayout.tsx / app/components/home/HomePageTopBar.tsx /
app/routes/profile._index.tsx: the BadgeCounts type was declared with
snake_case fields (order_updates, seller_orders) while the API's global
camelCase renderer sends orderUpdates / sellerOrders on the wire — every
read went to undefined, so the "buyer activity" dot on the profile icon
and the order-badge on the store dashboard were silently stuck at 0.
Fixed by renaming the interface to match the wire and adding sellerChat
(per-store roll-up) so the store dashboard sidebar reads
badges.sellerChat[storeId] instead of the buyer-side chat count.
- app/hooks/useBadgeCounts.ts: interface + EMPTY constant updated to
camelCase + new sellerChat map.
- app/utils/api-error-handler.ts: 403 no longer force-redirects to /logout.
It used to force-log-out on ANY 403/401, so an approved-but-unbadged
owner (Ehsan / Noelabel) whose store dashboard fired the Instagram-tab
query got a 403 → forced logout → the store page vanished mid-load
and the seller landed on the home screen convinced they'd been kicked
off their own store. Only 401 signs the session out now.
- app/components/store/StorePushPromptCard.tsx: new dashboard-level card
that nudges the store owner/staff to enable browser push. Unlike the
global buyer banner it's not dismissible-forever; "بعداً یادآوری کن"
hides for 24h and it comes back until the user actually subscribes.
Handles the permission="denied" path with an in-browser-settings hint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Review fixes:
- useMyStores throws on a failed response instead of resolving to [] — a
transient 401/blip no longer looks like "no stores" and bounces an owner off
their own dashboard (query stays in error/retry, keeps prior data).
- Add useRequireOwner() guard and apply it to the owner-only routes
(financial-dashboard + its cash-funds/reports/transactions sub-routes and
shipping-method) so staff who hit those URLs directly are redirected to the
dashboard. (Financial data was already backend-protected; this is the matching
client-side gate.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- useMyStores + team hooks (invite by phone / list / remove) in use-seller-hooks.
- useStoreOwnership rewritten to allow staff (not just the owner) via my-stores;
exposes `role` so callers can gate owner-only UI, and no longer redirects
members away from a store they help manage.
- Store settings: owner-only items (financial, shipping, edit store, instagram
sync, team) hidden from staff; new "مدیریت اعضای تیم" entry (owner-only).
- New /store/:storeId/team page: invite by phone + members list with pending/
active status + remove (owner-only, redirects staff). MyLayout keeps store mode
on the team route.
- StoreForm edit is owner-only (redirects staff). Profile "enter my store" now
routes owners to their store and staff to the store they manage.
- Chat already split earlier: seller inbox vs personal buyer inbox.
Depends on the seller-team backend; ships on this branch, not main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The backend view returns {"public_key": ...} but the global
CamelCaseJSONRenderer rewrites it to {"publicKey": ...} on the wire,
so destructuring `public_key` always resolved to undefined and the
subscribe path bailed silently. Browsers never registered with the
backend, no PushSubscription rows were ever created, and no push
notifications fired.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces three events without a full notification center:
- service worker (public/sw.js) gains push + notificationclick handlers
(RTL, focuses an existing tab or opens the target URL).
- usePushNotifications hook: permission + VAPID subscribe/unsubscribe
against the backend push endpoints.
- opt-in UI both ways: a one-time dismissible post-login banner
(PushPermissionBanner, rendered in MyLayout) and a permanent toggle
(PushSettingToggle) on buyer profile settings and seller store settings.
- useBadgeCounts polls /api/notif/v1/badges/; small red badges on the
bottom-nav profile icon (chat + order updates), the profile sidebar
(پیامها / سفارشها) and the seller dashboard sidebar. Visiting orders
clears its badge.
Degrades gracefully: if the backend endpoints are absent the counts fall
back to zero and nothing throws.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>