Skip to content

feat: implement #523 #524 #525 #526 — Web Push, timer drift fix, disc… - #529

Merged
Chuks-coderr merged 2 commits into
SoroStream:mainfrom
olawaleakanbi035-maker:feat/523-524-525-526-multi-fix
Aug 28, 2026
Merged

feat: implement #523 #524 #525 #526 — Web Push, timer drift fix, disc…#529
Chuks-coderr merged 2 commits into
SoroStream:mainfrom
olawaleakanbi035-maker:feat/523-524-525-526-multi-fix

Conversation

@olawaleakanbi035-maker

Copy link
Copy Markdown
Contributor

…onnect cache clear, deposit cap validation

Closes #523 — Web Push API + service worker push handler + per-event notification controls Closes #524 — Fix background timer drift with Page Visibility API Closes #525 — Clear stream data from UI on wallet disconnect Closes #526 — Validate deposit cap from contract before form submission

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #523 — In-app Web Push notifications for stream events ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Problem: The app had no Web Push integration. Users could not receive browser notifications for stream events when the tab was closed.

What was done:
• src/lib/pushSubscription.ts (new file)

  • isWebPushSupported() — guards for serviceWorker + PushManager + Notification
  • subscribeToPush() — registers the SW, requests Notification permission, then calls pushManager.subscribe() with the VAPID applicationServerKey derived from NEXT_PUBLIC_VAPID_PUBLIC_KEY (URL-safe base64 → Uint8Array)
  • unsubscribeFromPush() — calls subscription.unsubscribe() and clears the locally persisted subscription from localStorage
  • getActivePushSubscription() — returns the current PushSubscription or null
  • dispatchPushNotification(payload) — posts a sorostream-show-notification message to the active service worker so it can call showNotification(), with a direct Notification API fallback when no SW is active

• public/sw.js — added three new event listeners:

  • push: decodes JSON payload (or falls back to text), calls self.registration.showNotification() with icon, badge, tag, and a data object containing the click-through URL
  • notificationclick: closes the notification and either focuses an already- open matching window or opens a new one at the target URL
  • message (sorostream-show-notification): handles dispatches from the app layer, calling self.registration.showNotification() so that notifications work even without a real push server during development

• src/lib/notificationPrefs.ts

  • Added streamReceived and streamCancelled to NotificationEventPrefs so users can opt in to the two new event types introduced by this issue
  • DEFAULT_NOTIFICATION_PREFS updated to default both new events to true

• src/app/settings/notifications/page.tsx (complete rewrite)

  • Imports subscribeToPush, unsubscribeFromPush, getActivePushSubscription, dispatchPushNotification from the new pushSubscription library
  • On mount, calls getActivePushSubscription() to hydrate the isSubscribed badge so the UI reflects the real browser state
  • handleTogglePush(true): requests permission, subscribes via Web Push API (falls back gracefully when VAPID key absent), updates persisted pref
  • handleTogglePush(false): unsubscribes from Web Push, clears isSubscribed
  • 'Send test push notification' link calls dispatchPushNotification()
  • EVENT_LABELS extended to include all 5 events: streamReceived, withdrawalAvailable, streamCancelled, streamCompleted, expiringSoon

• .env.example — documented NEXT_PUBLIC_VAPID_PUBLIC_KEY with generation
instructions (npx web-push generate-vapid-keys)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #524 — Fix background timer drift in countdown components ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Problem: When a tab was backgrounded for > 30 s the browser throttled setInterval, causing the countdown to freeze then jump on refocus.

Root cause: Both CountdownTimer and StartCountdownTimer relied purely on accumulated interval ticks. A throttled interval accumulates a time deficit that shows as a sudden forward jump on refocus.

What was done (components/CountdownTimer.tsx, components/StartCountdownTimer.tsx): • Moved from import { useEffect, useState } to also import useRef so a stable
ref to the interval ID is kept across renders.
• Added a handleVisibilityChange listener on document.visibilitychange:

  • When the tab becomes hidden: clearInterval() — stop ticking to prevent further drift from accumulating.
  • When the tab becomes visible: call recalculate() immediately using Date.now() (which gives the correct elapsed time regardless of throttling), then startInterval() to resume ticking from a clean baseline. • recalculate() is extracted from the setInterval callback so the same
    function can be called synchronously on refocus and asynchronously on tick.
    • The event listener is cleaned up in the useEffect return function alongside
    the interval, preventing memory leaks.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #525 — Clear stream data from UI on wallet disconnect ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Problem: After disconnecting the wallet, stream detail and dashboard pages continued to display stream data from the previous session, creating a potential data-visibility issue for shared devices.

Dashboard (src/app/dashboard/page.tsx):
Already correctly calls setStreams([]) before the early return in the
address-keyed useEffect — confirmed no change required.

Stream detail (src/app/stream/[id]/page.tsx):
Added a new useEffect that watches the address value:
useEffect(() => {
if (address === null) {
setStream(null);
setHistoryEntries([]);
setError(null);
setAllStreams([]);
}
}, [address]);
When the wallet disconnects (address → null), stream, historyEntries,
error, and allStreams are all flushed immediately so stale data is never
visible to a subsequent user who connects on the same device.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #526 — Validate deposit cap before form submission ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Problem: The stream creation form allowed users to enter an amount above the contract's configured maximum deposit cap, resulting in a confusing transaction-failed error from the network after the user had already signed.

What was done:
• src/lib/sorostream.ts — added StreamCapConfig interface and
getStreamCapConfig() async function that reads the contract's per-stream
deposit cap (mocked at 100 000 USDC / 1 000 000 000 000 stroops). In
production this calls the contract's get_stream_cap query instruction.

• src/app/stream/new/page.tsx:

  • Imports getStreamCapConfig alongside the existing contract helpers.
  • New state: depositCapStroops (number | null), capLoading (boolean), capError (string).
  • New useEffect keyed on step === 'amount': fetches the cap when the user enters the amount step; clears cap state if step changes away.
  • Amount label row replaced with a flex row: left → existing label text right → 'Max: 100,000 USDC' indicator (hidden while loading or if no cap) right → 'Checking limit…' during fetch
  • goNext() at step === 'amount': after the existing format/range checks, converts the entered value to stroops and compares against depositCapStroops. If exceeded, sets errors.amount to a clear human-readable message ('Amount exceeds the contract's maximum deposit cap of X TOKEN') and returns early — the user never reaches the signing step.

Files changed:
.env.example
components/CountdownTimer.tsx
components/StartCountdownTimer.tsx
public/sw.js
src/app/settings/notifications/page.tsx
src/app/stream/[id]/page.tsx
src/app/stream/new/page.tsx
src/lib/notificationPrefs.ts
src/lib/pushSubscription.ts (new)
src/lib/sorostream.ts

…am#526 — Web Push, timer drift fix, disconnect cache clear, deposit cap validation

Closes SoroStream#523 — Web Push API + service worker push handler + per-event notification controls
Closes SoroStream#524 — Fix background timer drift with Page Visibility API
Closes SoroStream#525 — Clear stream data from UI on wallet disconnect
Closes SoroStream#526 — Validate deposit cap from contract before form submission

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SoroStream#523 — In-app Web Push notifications for stream events
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Problem: The app had no Web Push integration. Users could not receive
browser notifications for stream events when the tab was closed.

What was done:
• src/lib/pushSubscription.ts (new file)
  - isWebPushSupported() — guards for serviceWorker + PushManager + Notification
  - subscribeToPush() — registers the SW, requests Notification permission,
    then calls pushManager.subscribe() with the VAPID applicationServerKey
    derived from NEXT_PUBLIC_VAPID_PUBLIC_KEY (URL-safe base64 → Uint8Array)
  - unsubscribeFromPush() — calls subscription.unsubscribe() and clears
    the locally persisted subscription from localStorage
  - getActivePushSubscription() — returns the current PushSubscription or null
  - dispatchPushNotification(payload) — posts a sorostream-show-notification
    message to the active service worker so it can call showNotification(),
    with a direct Notification API fallback when no SW is active

• public/sw.js — added three new event listeners:
  - push: decodes JSON payload (or falls back to text), calls
    self.registration.showNotification() with icon, badge, tag, and a data
    object containing the click-through URL
  - notificationclick: closes the notification and either focuses an already-
    open matching window or opens a new one at the target URL
  - message (sorostream-show-notification): handles dispatches from the app
    layer, calling self.registration.showNotification() so that notifications
    work even without a real push server during development

• src/lib/notificationPrefs.ts
  - Added streamReceived and streamCancelled to NotificationEventPrefs so
    users can opt in to the two new event types introduced by this issue
  - DEFAULT_NOTIFICATION_PREFS updated to default both new events to true

• src/app/settings/notifications/page.tsx (complete rewrite)
  - Imports subscribeToPush, unsubscribeFromPush, getActivePushSubscription,
    dispatchPushNotification from the new pushSubscription library
  - On mount, calls getActivePushSubscription() to hydrate the isSubscribed
    badge so the UI reflects the real browser state
  - handleTogglePush(true): requests permission, subscribes via Web Push API
    (falls back gracefully when VAPID key absent), updates persisted pref
  - handleTogglePush(false): unsubscribes from Web Push, clears isSubscribed
  - 'Send test push notification' link calls dispatchPushNotification()
  - EVENT_LABELS extended to include all 5 events: streamReceived,
    withdrawalAvailable, streamCancelled, streamCompleted, expiringSoon

• .env.example — documented NEXT_PUBLIC_VAPID_PUBLIC_KEY with generation
  instructions (npx web-push generate-vapid-keys)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SoroStream#524 — Fix background timer drift in countdown components
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Problem: When a tab was backgrounded for > 30 s the browser throttled
setInterval, causing the countdown to freeze then jump on refocus.

Root cause: Both CountdownTimer and StartCountdownTimer relied purely on
accumulated interval ticks. A throttled interval accumulates a time deficit
that shows as a sudden forward jump on refocus.

What was done (components/CountdownTimer.tsx, components/StartCountdownTimer.tsx):
• Moved from import { useEffect, useState } to also import useRef so a stable
  ref to the interval ID is kept across renders.
• Added a handleVisibilityChange listener on document.visibilitychange:
  - When the tab becomes hidden: clearInterval() — stop ticking to prevent
    further drift from accumulating.
  - When the tab becomes visible: call recalculate() immediately using
    Date.now() (which gives the correct elapsed time regardless of throttling),
    then startInterval() to resume ticking from a clean baseline.
• recalculate() is extracted from the setInterval callback so the same
  function can be called synchronously on refocus and asynchronously on tick.
• The event listener is cleaned up in the useEffect return function alongside
  the interval, preventing memory leaks.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SoroStream#525 — Clear stream data from UI on wallet disconnect
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Problem: After disconnecting the wallet, stream detail and dashboard pages
continued to display stream data from the previous session, creating a
potential data-visibility issue for shared devices.

Dashboard (src/app/dashboard/page.tsx):
  Already correctly calls setStreams([]) before the early return in the
  address-keyed useEffect — confirmed no change required.

Stream detail (src/app/stream/[id]/page.tsx):
  Added a new useEffect that watches the address value:
    useEffect(() => {
      if (address === null) {
        setStream(null);
        setHistoryEntries([]);
        setError(null);
        setAllStreams([]);
      }
    }, [address]);
  When the wallet disconnects (address → null), stream, historyEntries,
  error, and allStreams are all flushed immediately so stale data is never
  visible to a subsequent user who connects on the same device.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SoroStream#526 — Validate deposit cap before form submission
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Problem: The stream creation form allowed users to enter an amount above
the contract's configured maximum deposit cap, resulting in a confusing
transaction-failed error from the network after the user had already signed.

What was done:
• src/lib/sorostream.ts — added StreamCapConfig interface and
  getStreamCapConfig() async function that reads the contract's per-stream
  deposit cap (mocked at 100 000 USDC / 1 000 000 000 000 stroops). In
  production this calls the contract's get_stream_cap query instruction.

• src/app/stream/new/page.tsx:
  - Imports getStreamCapConfig alongside the existing contract helpers.
  - New state: depositCapStroops (number | null), capLoading (boolean),
    capError (string).
  - New useEffect keyed on step === 'amount': fetches the cap when the user
    enters the amount step; clears cap state if step changes away.
  - Amount label row replaced with a flex row:
    left  → existing label text
    right → 'Max: 100,000 USDC' indicator (hidden while loading or if no cap)
    right → 'Checking limit…' during fetch
  - goNext() at step === 'amount': after the existing format/range checks,
    converts the entered value to stroops and compares against depositCapStroops.
    If exceeded, sets errors.amount to a clear human-readable message
    ('Amount exceeds the contract's maximum deposit cap of X TOKEN') and
    returns early — the user never reaches the signing step.

Files changed:
  .env.example
  components/CountdownTimer.tsx
  components/StartCountdownTimer.tsx
  public/sw.js
  src/app/settings/notifications/page.tsx
  src/app/stream/[id]/page.tsx
  src/app/stream/new/page.tsx
  src/lib/notificationPrefs.ts
  src/lib/pushSubscription.ts   (new)
  src/lib/sorostream.ts
@drips-wave

drips-wave Bot commented Aug 27, 2026

Copy link
Copy Markdown

@olawaleakanbi035-maker Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

@olawaleakanbi035-maker is attempting to deploy a commit to the Chuks7 Team on Vercel.

A member of the Team first needs to authorize it.

@Chuks-coderr
Chuks-coderr merged commit 53324dd into SoroStream:main Aug 28, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment