Skip to content

Frontend feat - #602

Merged
Idrhas merged 2 commits into
Fundable-Protocol:mainfrom
RaymondAbiola:frontend-feat
Aug 3, 2026
Merged

Frontend feat#602
Idrhas merged 2 commits into
Fundable-Protocol:mainfrom
RaymondAbiola:frontend-feat

Conversation

@RaymondAbiola

@RaymondAbiola RaymondAbiola commented Jul 30, 2026

Copy link
Copy Markdown

Closes #523
Closes #526

Summary by CodeRabbit

  • New Features

    • Added a guided four-step payment stream creation flow for campaign details, funding, scheduling, and review.
    • Added validation for campaign information, recipient addresses, amounts, tokens, durations, and stream settings.
    • Added step navigation, progress indicators, unsaved-change protection, fee estimates, and submission states.
    • Added real-time stream progress visualization with release rates, available funds, withdrawn amounts, countdowns, and progress tracking.
  • Tests

    • Added coverage for wizard validation, navigation, submission states, and real-time progress behavior.

@drips-wave

drips-wave Bot commented Jul 30, 2026

Copy link
Copy Markdown

@RaymondAbiola 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

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a real-time payment-stream progress visualizer and a four-step stream creation wizard with validation, navigation state, responsive UI states, and Vitest/React Testing Library coverage.

Changes

Stream progress visualization

Layer / File(s) Summary
Progress calculation and live ticking
apps/web/src/hooks/use-stream-progress.ts, apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.test.tsx
Computes streamed, withdrawn, available, remaining, percentage, rate, and completion values, with interval updates for active streams and validation tests.
Progress visualizer states and display
apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx, apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.test.tsx
Renders loading, unavailable, active, completed, and paused states with countdowns, progress bars, balances, and release-rate statistics.

Stream creation wizard

Layer / File(s) Summary
Wizard data and validation
apps/web/src/components/modules/payment-stream/wizard/wizard-config.ts
Defines wizard steps, form data, defaults, duration options, and campaign, funding, schedule, and review validation.
Wizard state and navigation
apps/web/src/hooks/use-stream-wizard.ts
Manages form data, errors, step transitions, reset behavior, dirty state, and validation-gated navigation.
Wizard steps and progress navigation
apps/web/src/components/modules/payment-stream/wizard/WizardSteps.tsx, apps/web/src/components/modules/payment-stream/wizard/WizardStepper.tsx
Renders the campaign, funding, schedule, and review steps plus step completion and navigation indicators.
Wizard orchestration and interaction tests
apps/web/src/components/modules/payment-stream/wizard/StreamWizard.tsx, apps/web/src/components/modules/payment-stream/wizard/StreamWizard.test.tsx
Connects wizard state to step components, completion and loading controls, unsaved-change tracking, and interaction tests.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StreamProgressVisualizer
  participant useStreamProgress
  participant computeStreamProgress
  StreamProgressVisualizer->>useStreamProgress: pass stream timing and amounts
  useStreamProgress->>computeStreamProgress: compute current progress
  useStreamProgress->>computeStreamProgress: recompute on active interval ticks
  computeStreamProgress-->>StreamProgressVisualizer: return progress values and status
Loading
sequenceDiagram
  participant Creator
  participant StreamWizard
  participant useStreamWizard
  participant WizardSteps
  Creator->>StreamWizard: enter wizard data
  StreamWizard->>WizardSteps: render current step
  StreamWizard->>useStreamWizard: validate and advance
  useStreamWizard-->>StreamWizard: return errors or next step
  Creator->>StreamWizard: submit final review
  StreamWizard->>StreamWizard: call onComplete with wizard data
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too generic and doesn't describe the wizard and progress visualizer work in this PR. Use a concise title that names the main feature, such as adding the payment stream wizard and progress visualizer.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The changes align with issues #523 and #526 by adding the progress visualizer, stream wizard, and related tests.
Out of Scope Changes check ✅ Passed The modified files all support the two linked frontend features, with no clear unrelated or extraneous changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx (2)

22-28: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Hardcoded 7-decimal precision ignores per-token granularity.

AMOUNT_PRECISION is applied to every tokenSymbol uniformly. Tokens with fewer decimals (e.g., 6-decimal USDC) will show spurious trailing precision, while others may need more. Consider accepting a decimals/precision prop derived from the token metadata instead of a fixed constant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx`
around lines 22 - 28, Update formatAmount and its callers in
StreamProgressVisualizer to use the token’s metadata-derived decimals/precision
rather than the global AMOUNT_PRECISION constant. Pass the appropriate token
granularity through the component props or existing token data, preserving
locale formatting while matching each token’s configured decimal precision.

9-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a narrower type for status.

status: string allows any value; only "active" is checked (line 81). A union/enum (e.g. "active" | "paused" | "completed" | "cancelled") would let the compiler catch typos and unhandled statuses, and would make it easier to correctly branch header/label rendering per state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx`
around lines 9 - 20, Update StreamProgressVisualizerProps.status from string to
a narrow status union covering the supported stream states, including active,
paused, completed, and cancelled. Align the component’s status checks and
rendering branches with this typed set while preserving the existing active
behavior and ensuring every allowed state has intentional handling.
apps/web/src/components/modules/payment-stream/wizard/StreamWizard.test.tsx (1)

25-116: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test coverage stops at the funding step.

No tests exercise the Schedule step (duration/duration-unit validation, toggles), the Review step, or a full happy-path run that reaches onComplete. Given this is a new, multi-step user flow, extending coverage through completion would catch regressions in the untested steps (including toggle wiring and duration validation).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/wizard/StreamWizard.test.tsx`
around lines 25 - 116, Extend the StreamWizard test suite beyond the funding
step to cover Schedule validation for duration and duration-unit fields,
schedule toggle behavior, Review rendering, and a complete valid flow that
reaches the onComplete callback. Reuse the existing renderWizard and navigation
helpers, and verify validation prevents advancement until corrected while the
happy path reaches completion with the expected data.
apps/web/src/components/modules/payment-stream/wizard/WizardSteps.tsx (1)

61-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Description textarea error isn't programmatically associated via aria-describedby.

aria-invalid is set but there's no aria-describedby linking to the FieldError text, so assistive tech users tabbing back into the field won't hear the current error state (only the initial live-region announcement).

♿ Suggested association
         <textarea
           id="campaign-description"
           ...
           aria-invalid={!!errors.campaignDescription}
+          aria-describedby={errors.campaignDescription ? "campaign-description-error" : undefined}
           onChange={(e) => updateField("campaignDescription", e.target.value)}
           className="..."
         />
-        <FieldError message={errors.campaignDescription} />
+        <FieldError id="campaign-description-error" message={errors.campaignDescription} />

(FieldError would need an optional id prop threaded onto its <p>.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/wizard/WizardSteps.tsx` around
lines 61 - 72, Update the campaign description textarea in the WizardSteps
component to set aria-describedby to a stable error-element ID, and pass that
same ID through FieldError so its rendered paragraph receives it. Preserve the
existing aria-invalid behavior and ensure the association is present for this
field’s validation message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx`:
- Around line 109-151: Update StreamProgressVisualizer.tsx around isStreaming to
derive an isCompleted state from progress.isComplete and render “Stream
completed” before the paused fallback. In StreamProgressVisualizer.test.tsx, add
coverage using status="active" with fake timers and now past endTime, asserting
the completed label appears and the paused label does not.

In `@apps/web/src/components/modules/payment-stream/wizard/StreamWizard.tsx`:
- Around line 37-53: Update handleNext to await onComplete after validating the
final step, handling rejected promises through the component’s existing
user-facing error mechanism instead of fire-and-forget. After onComplete
resolves successfully, call wizard.reset() so the completed wizard is no longer
dirty and useUnsavedChanges stops prompting.

In `@apps/web/src/components/modules/payment-stream/wizard/wizard-config.ts`:
- Around line 104-120: Update validateScheduleStep to require durationValue to
be an integer before calling validateEndTime, rejecting fractional values such
as "0.5" with the existing duration validation error. Preserve the current
required and positive-value checks, and only invoke validateEndTime for valid
positive integers.

In `@apps/web/src/components/modules/payment-stream/wizard/WizardSteps.tsx`:
- Around line 90-98: Move the errors.token rendering from its current location
after the Recipient Address field to immediately alongside the Token AppSelect
in WizardSteps, preserving the existing error content and styling while keeping
other field errors unchanged.

In `@apps/web/src/hooks/use-stream-progress.ts`:
- Around line 110-124: Remove the synchronous setNow(Date.now()) call from the
!isActive branch of the useEffect in use-stream-progress.ts, leaving the early
return intact. Preserve the active-stream interval updates and cleanup behavior.

---

Nitpick comments:
In `@apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx`:
- Around line 22-28: Update formatAmount and its callers in
StreamProgressVisualizer to use the token’s metadata-derived decimals/precision
rather than the global AMOUNT_PRECISION constant. Pass the appropriate token
granularity through the component props or existing token data, preserving
locale formatting while matching each token’s configured decimal precision.
- Around line 9-20: Update StreamProgressVisualizerProps.status from string to a
narrow status union covering the supported stream states, including active,
paused, completed, and cancelled. Align the component’s status checks and
rendering branches with this typed set while preserving the existing active
behavior and ensuring every allowed state has intentional handling.

In `@apps/web/src/components/modules/payment-stream/wizard/StreamWizard.test.tsx`:
- Around line 25-116: Extend the StreamWizard test suite beyond the funding step
to cover Schedule validation for duration and duration-unit fields, schedule
toggle behavior, Review rendering, and a complete valid flow that reaches the
onComplete callback. Reuse the existing renderWizard and navigation helpers, and
verify validation prevents advancement until corrected while the happy path
reaches completion with the expected data.

In `@apps/web/src/components/modules/payment-stream/wizard/WizardSteps.tsx`:
- Around line 61-72: Update the campaign description textarea in the WizardSteps
component to set aria-describedby to a stable error-element ID, and pass that
same ID through FieldError so its rendered paragraph receives it. Preserve the
existing aria-invalid behavior and ensure the association is present for this
field’s validation message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0da4877c-0c88-4ad9-a345-b0afc4aac2f8

📥 Commits

Reviewing files that changed from the base of the PR and between 375c936 and d2aa496.

📒 Files selected for processing (9)
  • apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.test.tsx
  • apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx
  • apps/web/src/components/modules/payment-stream/wizard/StreamWizard.test.tsx
  • apps/web/src/components/modules/payment-stream/wizard/StreamWizard.tsx
  • apps/web/src/components/modules/payment-stream/wizard/WizardStepper.tsx
  • apps/web/src/components/modules/payment-stream/wizard/WizardSteps.tsx
  • apps/web/src/components/modules/payment-stream/wizard/wizard-config.ts
  • apps/web/src/hooks/use-stream-progress.ts
  • apps/web/src/hooks/use-stream-wizard.ts

Comment on lines +109 to +151
const isStreaming = isActive && progress.hasStarted && !progress.isComplete;

return (
<section
aria-label="Real-time stream progress"
className={cn(
"w-full rounded-lg border border-zinc-700 bg-zinc-800/50 p-4 sm:p-6",
className
)}
>
<header className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<span
className={cn(
"relative flex h-2.5 w-2.5",
!isStreaming && "opacity-50"
)}
aria-hidden="true"
>
{isStreaming && (
<motion.span
className="absolute inline-flex h-full w-full rounded-full bg-green-400"
animate={{ scale: [1, 2.2, 1], opacity: [0.7, 0, 0.7] }}
transition={{ duration: 1.6, repeat: Infinity, ease: "easeOut" }}
/>
)}
<span
className={cn(
"relative inline-flex h-2.5 w-2.5 rounded-full",
isStreaming ? "bg-green-500" : "bg-zinc-500"
)}
/>
</span>
<h3 className="text-sm font-medium text-zinc-300">
{isStreaming ? "Streaming live" : "Stream paused"}
</h3>
</div>

<span className="flex items-center gap-1.5 font-mono text-xs text-zinc-400">
<Clock className="h-3.5 w-3.5" aria-hidden="true" />
{formatCountdown(progress.secondsRemaining)}
</span>
</header>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Root cause: no distinct "completed" state rendering, so it's also untested. isStreaming's negation drives the header label for both "paused" and "genuinely completed" cases, so a stream that finished naturally (progress.isComplete === true, status still "active") is shown as "Stream paused." The test suite has no case for this because the behavior doesn't exist yet.

  • apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx#L109-L151: add an isCompleted = progress.isComplete branch and render a distinct "Stream completed" label instead of falling through to "Stream paused."
  • apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.test.tsx#L61-L117: once fixed, add a test rendering with status="active" and a now past endTime (via fake timers) asserting the "completed" label appears instead of "paused."
📍 Affects 2 files
  • apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx#L109-L151 (this comment)
  • apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.test.tsx#L61-L117
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/StreamProgressVisualizer.tsx`
around lines 109 - 151, Update StreamProgressVisualizer.tsx around isStreaming
to derive an isCompleted state from progress.isComplete and render “Stream
completed” before the paused fallback. In StreamProgressVisualizer.test.tsx, add
coverage using status="active" with fake timers and now past endTime, asserting
the completed label appears and the paused label does not.

Comment on lines +37 to +53
const wizard = useStreamWizard({
defaultToken: tokenOptions[0]?.value ?? "XLM",
defaultDurationUnit: "day",
});

useUnsavedChanges(wizard.isDirty && !isSubmitting);

const handleNext = () => {
if (wizard.isLastStep) {
if (wizard.validateCurrentStep()) {
void onComplete(wizard.data);
}
return;
}

wizard.goNext();
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

handleNext doesn't handle onComplete rejection or reset state after success.

void onComplete(wizard.data) (Line 47) fires-and-forgets an async callback whose signature explicitly allows Promise<void> — a rejection (e.g., a failed on-chain transaction) becomes an unhandled promise rejection with no user-facing feedback. Separately, wizard.reset() is never called after a successful completion, so wizard.isDirty stays true and useUnsavedChanges (Line 42) will continue prompting "leave page?" warnings even though the stream was already created successfully.

🔧 Suggested fix
   const handleNext = () => {
     if (wizard.isLastStep) {
       if (wizard.validateCurrentStep()) {
-        void onComplete(wizard.data);
+        Promise.resolve(onComplete(wizard.data))
+          .then(() => wizard.reset())
+          .catch((error) => {
+            console.error("Failed to create stream", error);
+          });
       }
       return;
     }

     wizard.goNext();
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const wizard = useStreamWizard({
defaultToken: tokenOptions[0]?.value ?? "XLM",
defaultDurationUnit: "day",
});
useUnsavedChanges(wizard.isDirty && !isSubmitting);
const handleNext = () => {
if (wizard.isLastStep) {
if (wizard.validateCurrentStep()) {
void onComplete(wizard.data);
}
return;
}
wizard.goNext();
};
const wizard = useStreamWizard({
defaultToken: tokenOptions[0]?.value ?? "XLM",
defaultDurationUnit: "day",
});
useUnsavedChanges(wizard.isDirty && !isSubmitting);
const handleNext = () => {
if (wizard.isLastStep) {
if (wizard.validateCurrentStep()) {
Promise.resolve(onComplete(wizard.data))
.then(() => wizard.reset())
.catch((error) => {
console.error("Failed to create stream", error);
});
}
return;
}
wizard.goNext();
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/wizard/StreamWizard.tsx`
around lines 37 - 53, Update handleNext to await onComplete after validating the
final step, handling rejected promises through the component’s existing
user-facing error mechanism instead of fire-and-forget. After onComplete
resolves successfully, call wizard.reset() so the completed wizard is no longer
dirty and useUnsavedChanges stops prompting.

Comment on lines +104 to +120
function validateScheduleStep(data: WizardStreamData): WizardErrors {
const errors: WizardErrors = {};

const durationValue = Number(data.duration);
if (!data.duration.trim()) {
errors.duration = "Duration is required";
} else if (!Number.isFinite(durationValue) || durationValue <= 0) {
errors.duration = "Duration must be greater than 0";
} else {
const endTimeError = validateEndTime(null, data.duration, data.durationUnit);
if (endTimeError) {
errors.duration = endTimeError;
}
}

return errors;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate target file and relevant symbols"
fd -a 'wizard-config\.ts$|ScheduleStep\.(tsx|ts)$' . | sed 's#^\./##'

echo
echo "Inspect target file around validateScheduleStep and validateEndTime references"
target="$(fd 'wizard-config\.ts$' . | head -n 1)"
if [ -n "$target" ]; then
  wc -l "$target"
  sed -n '1,180p' "$target" | nl -ba
fi

echo
echo "Search for validateEndTime implementation/usages"
rg -n "function validateEndTime|const validateEndTime|validateEndTime|Duration must be greater" .

Repository: Fundable-Protocol/stellar_client_os

Length of output: 501


🏁 Script executed:

#!/bin/bash
set -euo pipefail

target="/home/jailuser/git/apps/web/src/components/modules/payment-stream/wizard/wizard-config.ts"

echo
echo "Target file lines with line numbers"
awk '{printf "%6d  %s\n", NR, $0}' "$target"

echo
echo "ScheduleStep candidate files and relevant parsing/end-time code"
fd 'ScheduleStep\.(tsx|ts)$' . | while read -r f; do
  echo "--- $f"
  wc -l "$f"
  rg -n "calculateEndTime|duration|startTime|useMemo|type=\"number\"|step=\"|parse|Number\\(" "$f" -C 3
done

echo
echo "Runtime probe for current input semantics"
node - <<'JS'
const inputs = ["0", "0.5", "1.5", "abc", " ", "1", "-1"];
for (const v of inputs) {
  const n = Number(v);
  console.log(JSON.stringify({ input: v, isFinite: Number.isFinite(n), pos: n > 0, parseInt: parseInt(v), NumberInteger: Number.isInteger(n) }));
}
JS

Repository: Fundable-Protocol/stellar_client_os

Length of output: 6215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find stream-validation file"
fd 'stream-validation\.(ts|tsx)$|stream-validation$' .

validation_file="$(fd 'stream-validation\.ts$' . | head -n 1 || true)"
if [ -n "$validation_file" ]; then
  echo "--- $validation_file"
  awk '{printf "%6d  %s\n", NR, $0}' "$validation_file"
fi

schedule_step="$(fd 'ScheduleStep\.(tsx|ts)$' . | head -n 1 || true)"
if [ -n "$schedule_step" ]; then
  echo "--- schedule step parsing/end-time references"
  rg -n "Duration|duration|start|endTime|calculate|validateWizardStep|type=\"number\"|step=\"|Number\\(|parseInt" "$schedule_step" -C 3
fi

# Probe all likely candidate files containing stream duration end-time parsing
rg -n "validateEndTime|calculateEndTime|Duration must be greater|Number\\(.*duration|durationUnit|startTime|endTime" . \
  -g '*.ts' -g '*.tsx' -g '!**/node_modules/**' -g '!**/.next/**' -C 2

Repository: Fundable-Protocol/stellar_client_os

Length of output: 50393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find ScheduleStep candidates"
fd 'ScheduleStep\.tsx$|ScheduleStep\.ts$' .

schedule="$(fd 'ScheduleStep\.tsx$|ScheduleStep\.ts$' . | head -n 1 || true)"
if [ -n "$schedule" ]; then
  echo "--- $schedule"
  wc -l "$schedule"
  rg -n "duration|calculateEndTime|validateWizardStep|Input|type=\"number\"|step=\"|parse|int|Number\\(|preview|endTime|formatEndTime|getRelativeTime|useMemo" "$schedule" -C 3
  echo "--- lines 80-150"
  awk '{printf "%6d  %s\n", NR, $0}' "$schedule" | sed -n '80,150p'
fi

echo
echo "Find wizard ScheduleStep references and local calculations"
rg -n "calculateEndTime|calculateStreamEndTime|formatEndTime|getRelativeTime|durationValue|durationUnit|wizard-config|validateWizardStep.*schedule|stepId === \"schedule\"" apps/web/src/components/modules/payment-stream -g '*.tsx' -g '*.ts' | head -n 80

Repository: Fundable-Protocol/stellar_client_os

Length of output: 7196


🏁 Script executed:

#!/bin/bash
set -euo pipefail

wizard_steps="/home/jailuser/git/apps/web/src/components/modules/payment-stream/wizard/WizardSteps.tsx"
form="/home/jailuser/git/apps/web/src/components/modules/payment-stream/PaymentStreamForm.tsx"
stepper="/home/jailuser/git/apps/web/src/components/modules/payment-stream/wizard/WizardStepper.tsx"

for f in "$wizard_steps" "$form" "$stepper"; do
  if [ -f "$f" ]; then
    echo "--- $f"
    wc -l "$f"
  fi
done

if [ -f "$wizard_steps" ]; then
  echo "--- WizardSteps relevant sections"
  awk '{printf "%6d  %s\n", NR, $0}' "$wizard_steps" | sed -n '1,280p'
fi

if [ -f "$form" ]; then
  echo "--- PaymentStreamForm relevant sections"
  awk '{printf "%6d  %s\n", NR, $0}' "$form" | sed -n '1,260p'
fi

Repository: Fundable-Protocol/stellar_client_os

Length of output: 22116


Reject non-integers in the wizard before calling validateEndTime.

validateScheduleStep() accepts values like "0.5" via Number.parse, but validateEndTime() parses via parseInt(...), so "0.5" is converted to 0 and fails with Duration must be greater than zero. The schedule preview also computes with Number(...), so fractional inputs can show an end-time preview while validation blocks progress. Add integer parsing at the source of truth and reject non-integers there.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/wizard/wizard-config.ts`
around lines 104 - 120, Update validateScheduleStep to require durationValue to
be an integer before calling validateEndTime, rejecting fractional values such
as "0.5" with the existing duration validation error. Preserve the current
required and positive-value checks, and only invoke validateEndTime for valid
positive integers.

Comment on lines +90 to +98
<AppSelect
className="h-12"
titleClassName="text-zinc-300"
title="Token"
options={tokenOptions}
value={data.token}
setValue={(value) => updateField("token", value)}
placeholder={data.token || "Select a token"}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Token error rendered far from the Token select it describes.

errors.token (Line 121) is displayed after the Recipient Address field, not near the AppSelect for Token (Lines 90-98), which can mislead users about which field the error applies to.

💡 Move the token error next to its field
         <AppSelect
           className="h-12"
           titleClassName="text-zinc-300"
           title="Token"
           options={tokenOptions}
           value={data.token}
           setValue={(value) => updateField("token", value)}
           placeholder={data.token || "Select a token"}
         />
+        <FieldError message={errors.token} />
         <InputWithLabel
           title="Total Amount"
           ...
         />
       </div>

       <InputWithLabel
         title="Recipient Address"
         ...
       />
-
-      <FieldError message={errors.token} />
     </div>

Also applies to: 121-121

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/components/modules/payment-stream/wizard/WizardSteps.tsx` around
lines 90 - 98, Move the errors.token rendering from its current location after
the Recipient Address field to immediately alongside the Token AppSelect in
WizardSteps, preserving the existing error content and styling while keeping
other field errors unchanged.

Comment on lines +110 to +124
useEffect(() => {
if (!isActive) {
setNow(Date.now());
return;
}

const interval = setInterval(() => {
const current = Date.now();
setNow(current);
// Nothing left to accrue once the stream ends — stop burning frames.
if (current >= endTime) clearInterval(interval);
}, tickMs);

return () => clearInterval(interval);
}, [isActive, tickMs, endTime]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Drop the synchronous setNow call inside the effect body.

Static analysis flags this correctly: calling setNow(Date.now()) directly in the effect body (line 112) causes an avoidable extra render on mount/every isActive/tickMs/endTime change for paused streams. Since the interval already keeps now within tickMs (100ms) of real time, this branch's sync-then-return isn't needed to "freeze" state accurately — removing it only introduces at most ~100ms of staleness, which is imperceptible, while eliminating the cascading-render risk.

⚡ Proposed fix
   useEffect(() => {
-    if (!isActive) {
-      setNow(Date.now());
-      return;
-    }
+    if (!isActive) return;
 
     const interval = setInterval(() => {
       const current = Date.now();
       setNow(current);
       // Nothing left to accrue once the stream ends — stop burning frames.
       if (current >= endTime) clearInterval(interval);
     }, tickMs);
 
     return () => clearInterval(interval);
   }, [isActive, tickMs, endTime]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!isActive) {
setNow(Date.now());
return;
}
const interval = setInterval(() => {
const current = Date.now();
setNow(current);
// Nothing left to accrue once the stream ends — stop burning frames.
if (current >= endTime) clearInterval(interval);
}, tickMs);
return () => clearInterval(interval);
}, [isActive, tickMs, endTime]);
useEffect(() => {
if (!isActive) return;
const interval = setInterval(() => {
const current = Date.now();
setNow(current);
// Nothing left to accrue once the stream ends — stop burning frames.
if (current >= endTime) clearInterval(interval);
}, tickMs);
return () => clearInterval(interval);
}, [isActive, tickMs, endTime]);
🧰 Tools
🪛 ESLint

[error] 112-112: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/apps/web/src/hooks/use-stream-progress.ts:112:7
110 | useEffect(() => {
111 | if (!isActive) {

112 | setNow(Date.now());
| ^^^^^^ Avoid calling setState() directly within an effect
113 | return;
114 | }
115 |

(react-hooks/set-state-in-effect)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/hooks/use-stream-progress.ts` around lines 110 - 124, Remove the
synchronous setNow(Date.now()) call from the !isActive branch of the useEffect
in use-stream-progress.ts, leaving the early return intact. Preserve the
active-stream interval updates and cleanup behavior.

Source: Linters/SAST tools

@Idrhas

Idrhas commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

dont forget to offramp using https://stellar.fundable.finance/offramp its fast, free and p2p rates

@Idrhas
Idrhas merged commit 9ef4bc5 into Fundable-Protocol:main Aug 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Frontend] Build Step-by-Step Campaign Funding & Stream Creation Wizard [Frontend] Create Animated Real-Time Payment Stream Progress Visualizer

2 participants