Dev - #165
Conversation
- course-create-form.jsx: fixed duplicated htmlFor=title on all four labels (description/category/price labels pointed at the title input instead of their own field); added matching id to Input/Textarea; fixed Label id=thumbail/id=file typos to htmlFor=thumbnail/file so the upload section labels actually associate. - ComboBox.jsx (CategoryCombobox): added an id prop forwarded to the trigger button so the course-category label can associate with it. - Searchbox.jsx: added a visually-hidden (sr-only) label wired via useId, giving the search input an accessible name beyond just a placeholder. - TransactionHistory.jsx: added an aria-label to the icon-only explorer link and aria-hidden to its icon. - ReelActionButton.jsx: added aria-label to the button and aria-hidden to the icon span, so icon-only reel actions have an accessible name. StarRate.jsx already had a proper aria-label/role=img for read-only ratings, so left untouched. Scope note: this covers the concrete labeling/accessible-name items from #134 pointers. Contrast audit, skip-to-content link, shell landmarks, and wiring an automated a11y check are separate, larger pieces of this issue not yet addressed here. npm run lint and npm run build both pass.
…progress bars - Add useCourseProgress hook with localStorage write-behind and throttled API writes - Add useAllCourseProgress for batched progress on course listing pages - Extend VidPlayerBox with startTime, onTimeUpdate, and onEnded props - Add resume/start-over UX on course detail page with progress indicator - Add progress bars and completion badges to CourseCard for owned courses - Graceful degradation: works via localStorage when backend endpoints unavailable - Completion marked at >=90% watched or on player ended event Closes #108
…tracking feat(course-progress): add resume playback, completion tracking, and progress bars
… (CodeRabbit)
label holds the visible count (e.g. 42), not an action name, so
aria-label={label} announced a bare number with no context for
like/love/comments/share buttons - or nothing at all where label
was numeric 0. Added a separate accessibleLabel prop used for
aria-label, and set it to Like/Love/Comments/Share at all 8
ReelActionButton call sites (desktop + mobile layouts) in
ReelCard.jsx. label remains the visible count only.
…rding Revert 137 feat/wallet onboarding
…deRabbit)
- accessibleLabel now folds in the visible count (e.g. "Like, 42")
instead of just the action name, so screen readers announce both.
- Added a pressed prop to ReelActionButton that renders aria-pressed,
wired to pressed={viewerLiked}/{viewerLoved} on the like/love
instances only, so assistive tech can tell whether a reaction is
currently active. Not applied to comments/share since those are not
toggle buttons.
- Add /educators/[profileid] public route accessible without authentication - Add EducatorProfileHeader with avatar, bio, role, stats, follower count - Add PublicCourseCard, PublicBookCard, PublicSpaceCard (no auth dependencies) - Add tabbed navigation for courses, books, and spaces - Add share button with Web Share API and clipboard fallback - Add auth-aware follow: logged-out users redirect to /login?next=... - Add privacy guard: shows not-found for missing/private profiles - Rewire instructor links on courseCard, libraryCard, spaceCard to /educators/[id] - Add 'View public page' link on authenticated profile page - Add generateMetadata for SEO (title, description, OpenGraph) Closes #116
…, and user rejection - Add lib/stellar/stellarErrors.js: reusable error mapping utility - StellarProvider: wallet detection, network mismatch check, validateForPayment() - PaymentModal: no-wallet install prompts, network mismatch warning, trustline/balance checks - WalletConnectButton: install prompts when no extension, network mismatch badge - useStellarPayment: graceful user rejection handling
fix: a11y label associations and accessible names (#134)
…storefront feat(educators): add public educator storefront pages
- Replace hardcoded files.vidstack.io demo subtitles/thumbnails with real course captions, sourced only from data.subtitles/data.chapters - Add chapter markers (VTT or array) with graceful degrade when absent - Persist playback rate/volume/muted to localStorage across courses - Add clear error state for missing/broken video instead of blank player - Add discoverable keyboard-shortcuts help and a focusable, labelled player - Lazy-load the player component to keep Vidstack off the initial bundle Progress/resume tracking intentionally left to #108.
… shortcuts and type grouping
…t in dashboard layout
…closing via shortcut
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Await params and cookies() in app/api/books/[bookId]/preview/route.js - Await params in 4 server component pages: - dashboard/courses/[courseId]/page.jsx - dashboard/library/[bookid]/page.jsx - dashboard/library/read/[bookid]/page.jsx - dashboard/spaces/[spacesid]/page.jsx - Use React.use(params) in client component dashboard/search/[searchparam]/page.jsx
fix: await dynamic APIs for Next.js 15 compatibility
- Error vs empty state: getTransactionHistory now returns error message; TransactionHistory tracks error state and renders an inline error card with 'Try Again' button instead of misleading 'No transactions found' - Role page reset: handleRoleChange resets pagination to page 1 so switching buyer/creator always fetches the first page - Header always mounted: role select and title stay interactive during loading; only the table area skeletons; pagination buttons disabled while fetch is in flight - exhaustive-deps: fetchTransactions wrapped in useCallback with proper deps; effect depends only on fetchTransactions
Throw error if network request fails in development mode.
…ror-handling fix: error handling, role reset, and loading UX in TransactionHistory
…ersal-search feat: implemented global command palette and universal search
- wire StatsOverview to use live data from useStats() - add loading skeletons, error state, and retry support for stats - correct dashboard icon mapping for all stat cards - fetch and display real upcoming sessions from the backend - add loading and empty states for upcoming sessions - link "View Upcomings" to the dashboard spaces page - remove hardcoded learning progress chart data and misleading metrics - add honest placeholder state pending backend activity data - prepare chart with distinct series configuration for future monthly analytics - clean up unused imports and ensure successful build
feat(dashboard): replace placeholder widgets with real dashboard data
- replace hand-rolled portal Modal with Radix Dialog primitives - add role=dialog, aria-modal, and aria-labelledby via DialogPrimitive - inherit Radix focus trap, focus restore, and scroll lock - overlay click and Escape close now work as expected - add aria-label=Close to the close button - honor className prop on the content container (fixes Notybell sizing) - remove AnimatePresence wrapper from Notybell (Radix handles lifecycle) - clean up unused imports - preserve existing visual style (bg-accent header, scrollable body) - lint and build pass cleanly
fix(modal): reimplement Modal over Radix Dialog for full accessibility
…dead-components chore(deps): remove unused dependencies and dead components
…agination feat(library): add search, filters, sorting, and pagination
- Rewrite .env.example with only the variables the code reads; remove NEXT_PUBLIC_CLOUDINARY_API_SECRET, _API_KEY, _URL, DNB_API_URL, NEXT_PUBLIC_SOCKET_URL - Add lib/config/env.js with Zod schema that validates URLs, constrains STELLAR_NETWORK to testnet|mainnet, rejects NEXT_PUBLIC_*SECRET vars, and fails build with aggregated error messages - Source Firebase config from env vars with current values as defaults - Route all process.env reads through the validated config object - Update README configuration table with all variables and required column
The Zod schema required NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, causing the build to fail when the variable is not set in CI. The runtime code in cloudinaryUpload.js already validates the cloud name and throws a clear error, so the schema should allow undefined and warn at access time.
…validation feat: secure and validate environment configuration
…tencies
- Create AuthProvider (components/providers/AuthProvider.jsx) that reads cookies once and owns user/isAuthenticated/loading/logout/refreshUser state via React context
- Rewrite hooks/useAuth.js to consume the context (same return shape preserved)
- Mount AuthProvider in app/layout.js above StellarProvider
- Normalize all cookie writes to use { expires: 1, path: '/' } in login, signup, and refreshUser
- Wrap userInfo JSON.parse in try/catch; on failure clear cookies and treat as logged out
- Add module-level callback mechanism so login/signup update context state immediately after setting cookies, eliminating the need for setTimeout workarounds
Fixes #73
- Destructure cancelPayment from useStellarPayment and call it when
modal closes with a pending, unsubmitted transaction (confirm step
or Back button)
- Modify executePayment to return { success, cancelled, data } so the
modal can distinguish wallet rejection from other failures
- On wallet rejection: cancel the pending backend transaction and
return user to preview step instead of leaving the tx dangling
- Block overlay click and Escape dismissal during processing
(onPointerDownOutside / onEscapeKeyDown preventDefault)
- Update processing text to 'Waiting for wallet confirmation…'
- Add closingRef guard to make handleClose safe for multiple calls
- Back button now cancels the pending tx, ensuring initiate-back-
initiate does not create orphaned duplicate transactions
Fixes #76
- book-create-form.jsx: RHF + zod schema (title, description, category, price); label/input associations fixed via shadcn Form primitives; file validation using validateFile helper; currency changed from NGN to USDC; removed alert() calls, replaced with inline errors + toast - course-create-form.jsx: RHF + zod schema; CategoryCombobox integrated via Controller; file validation on thumbnail/video; currency to USDC - space-create-form.jsx: RHF + zod schema; DatePicker + TimePicker via Controller; price placeholder to USDC; removed alert() call - signup-form.jsx: RHF + zod schema with password match refinement; inline per-field errors before submit; fixed ErrorMessage prop name - login-form.jsx: RHF + zod schema; inline email/password validation
feat: migrate all forms to react-hook-form + zod validation
[Bug] PaymentModal abandons pending Stellar transactions instead of calling the existing cancelPayment
[Bug] Centralize auth state in a context provider and fix cookie inconsistencies
feat(#127): drive video player from real course data
feat(auth): add Sign in with Stellar SEP-10 wallet login #101
fix(wallet): handle missing wallet, wrong network, trustline, balance, and user rejection
# Conflicts: # components/atoms/dashboard/Notybell.jsx # components/atoms/dashboard/Searchbox.jsx # components/molecules/Modal.js # components/molecules/dashboard/nav-header.jsx # components/organisms/dashboard/StatsOverview.jsx # components/organisms/dashboard/UpcomingSessions.jsx
# Conflicts: # app/layout.js
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR centralizes environment configuration and expands authentication, Stellar wallet handling, educator profiles, course progress, library search, command search, form validation, and dashboard data loading. ChangesCentralized configuration
Authentication and Stellar flows
Educator and course experiences
Library and dashboard interactions
Content creation forms
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 Biome (2.5.5)app/layout.jsFile contains syntax errors that prevent linting: Line 76: Expected a JSX Expression, a Element, or a text but instead found '<<<<<<'.; Line 77: expected components/organisms/create/course-create-form.jsxFile contains syntax errors that prevent linting: Line 185: expected ... [truncated 1284 characters] ... und 🔧 ESLint
app/layout.jsParsing error: Unexpected token (76:1) Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/stellar/TransactionHistory.jsx (1)
53-72: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd a cancellation guard before applying asynchronous results.
When
roleorpagination.pagechanges, an older request can resolve later and overwrite the newer transaction list. Skip all state updates from stale requests.🤖 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 `@components/stellar/TransactionHistory.jsx` around lines 53 - 72, Update fetchTransactions in TransactionHistory to track whether its request is still current and skip all state updates from stale requests, including transactions, pagination, error, and loading state. Ensure the guard is invalidated when the effect reruns or unmounts, while preserving updates from the latest request.
🟠 Major comments (22)
components/molecules/dashboard/cards/educators/PublicCourseCard.jsx-14-21 (1)
14-21: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winEvery new public card marks its image as
priority. The educator page renders these cards in a grid, so all card images preload eagerly and compete with the true LCP element.
components/molecules/dashboard/cards/educators/PublicCourseCard.jsx#L14-L21: removepriorityand add asizesvalue that matches the 1/2/3-column grid.components/molecules/dashboard/cards/educators/PublicSpaceCard.jsx#L38-L45: removepriorityand add the samesizesvalue.🤖 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 `@components/molecules/dashboard/cards/educators/PublicCourseCard.jsx` around lines 14 - 21, Update the Image elements in PublicCourseCard and PublicSpaceCard to remove priority and add the same responsive sizes value matching the educator page’s 1/2/3-column grid. Apply the change in components/molecules/dashboard/cards/educators/PublicCourseCard.jsx lines 14-21 and components/molecules/dashboard/cards/educators/PublicSpaceCard.jsx lines 38-45.app/(pages)/educators/[profileid]/EducatorPageClient.jsx-187-191 (1)
187-191: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winSet
activeTabto the first available tab, otherwise the content area renders empty.
activeTabinitializes to"courses". If the educator publishes only books or only spaces,courses.lengthis 0, sotabscontains one entry and the tab bar stays hidden (Line 219). Every grid condition at Lines 245-265 then fails, and the visitor sees the header with an empty content area.Derive the effective tab from the available tabs.
🐛 Proposed fix
const tabs = [ { key: "courses", label: "Courses", count: courses.length, icon: GraduationCap }, { key: "books", label: "Books", count: books.length, icon: BookOpen }, { key: "spaces", label: "Spaces", count: spaces.length, icon: Users }, ].filter((t) => t.count > 0); + + const currentTab = tabs.some((t) => t.key === activeTab) + ? activeTab + : tabs[0]?.key;Then use
currentTabin the tab styling and in the three grid conditions:- {activeTab === "courses" && courses.length > 0 && ( + {currentTab === "courses" && courses.length > 0 && (🤖 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 `@app/`(pages)/educators/[profileid]/EducatorPageClient.jsx around lines 187 - 191, Derive a currentTab from the filtered tabs, falling back to the existing activeTab when it is available and otherwise selecting the first tab’s key. Update the tab styling and all three content-grid conditions in EducatorPageClient to use currentTab so educators without courses still display their available books or spaces.app/(pages)/educators/[profileid]/EducatorPageClient.jsx-47-98 (1)
47-98: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winGuard the state writes after
awaitwith a cancellation flag.The effect depends on
currentUser?._id. Authentication resolves asynchronously, so the dependency changes fromundefinedto an id and the effect re-runs. Two in-flightload()calls can then resolve out of order and write stale educator, course, or follow state. The static analysis hint reports the same pattern.🐛 Proposed fix
useEffect(() => { + let cancelled = false; async function load() { setLoading(true); setError(false); try { const res = await getUserById(profileid); + if (cancelled) return; const u = res?.user || null; if (!u) { setError(true); setLoading(false); return; } setEducator(u); @@ ]); + if (cancelled) return; @@ if (currentUser?._id && currentUser._id !== profileid) { const followRes = await checkIfFollowing(profileid); - if (followRes?.success) { + if (!cancelled && followRes?.success) { setIsFollowing(followRes.isFollowing); } } } catch (e) { - setError(true); + if (!cancelled) setError(true); } finally { - setLoading(false); + if (!cancelled) setLoading(false); } } load(); + return () => { + cancelled = true; + }; }, [profileid, currentUser?._id]);🤖 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 `@app/`(pages)/educators/[profileid]/EducatorPageClient.jsx around lines 47 - 98, Update the useEffect load flow to track cancellation for each effect invocation and guard every state update after an await, including educator, courses, books, spaces, follower count, follow status, errors, and loading. Set the cancellation flag in the effect cleanup so an earlier load cannot overwrite state after profileid or currentUser?._id changes, while preserving the existing loading and error behavior for the active invocation.Source: Linters/SAST tools
components/organisms/educators/EducatorProfileHeader.jsx-59-65 (1)
59-65: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winValidate API dates before calling
format. IfcreatedAtoreventDateis invalid,date-fns/formatthrowsRangeErrorand can break the public render. Checkparsed.getTime()withNumber.isNaN; omit the joined line or use"TBD"for invalid dates. A sharedformatDateSafehelper can cover both sites.🤖 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 `@components/organisms/educators/EducatorProfileHeader.jsx` around lines 59 - 65, Validate parsed dates before calling date-fns format in both EducatorProfileHeader and PublicSpaceCard. Add or reuse a shared formatDateSafe helper that checks parsed.getTime() with Number.isNaN, then omit the joined-date line for invalid createdAt values and display "TBD" for invalid eventDate values as appropriate.components/organisms/create/book-create-form.jsx-28-28 (1)
28-28: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winAn empty price passes validation as
0in all three create forms. Each schema usesz.coerce.number().min(0, ...)while the matching default value is the empty string.Number('')is0, and0satisfiesmin(0), so a user can submit the form without entering a price and the item is created for free. Apply the samez.preprocessguard in each schema so an empty value fails validation with "Price is required".
components/organisms/create/book-create-form.jsx#L28: wrap thepriceschema inz.preprocessand map'',null, andundefinedtoundefined.components/organisms/create/course-create-form.jsx#L31: apply the samez.preprocessguard to thepriceschema.components/organisms/create/space-create-form.jsx#L30: apply the samez.preprocessguard to thepriceschema.🤖 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 `@components/organisms/create/book-create-form.jsx` at line 28, Prevent empty prices from being coerced to zero by wrapping each price schema in a z.preprocess guard that maps empty strings, null, and undefined to undefined, then validates the value as required with the existing range constraints and “Price is required” message. Apply this consistently in components/organisms/create/book-create-form.jsx:28, components/organisms/create/course-create-form.jsx:31, and components/organisms/create/space-create-form.jsx:30.lib/config/env.js-75-90 (1)
75-90: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winFail startup when production service URLs are absent.
Lines 75-90 return localhost URLs when configuration is missing. In production, server routes will call a local service and client-side AI requests will target the user’s machine. Require both URLs in production and retain these fallbacks only for local development.
🤖 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 `@lib/config/env.js` around lines 75 - 90, Update the apiUrl and aiApiUrl getters in config to require NEXT_PUBLIC_API_URL and NEXT_PUBLIC_AI_API_URL when running in production, failing startup instead of returning localhost fallbacks; preserve the existing warning and localhost defaults for local development.lib/config/env.js-111-130 (1)
111-130: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRemove the tracked Firebase project configuration.
The shared root cause is embedding deployed Firebase values in repository files. Use deployment-managed environment variables and placeholder values only.
lib/config/env.js#L111-L130: remove the Firebase fallback literals, including the API key, and validate required configuration..env.example#L24-L30: replace deployed project values with non-production placeholders.Firebase browser configuration can be public, but this repository policy still prohibits hardcoded API keys. As per path instructions, “
**/*.{js,jsx}: Flag hardcoded secrets, API keys, or wallet secret keys.”🤖 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 `@lib/config/env.js` around lines 111 - 130, Remove all deployed Firebase fallback literals from the firebase configuration getter in lib/config/env.js#L111-L130, including the API key, and require validation of the corresponding environment variables instead. Replace the deployed Firebase values in .env.example#L24-L30 with clearly non-production placeholders; both sites require direct changes while preserving the existing Firebase configuration shape.Sources: Path instructions, Linters/SAST tools
hooks/useAuth.js-7-7 (1)
7-7: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winSession cookies are written without
secureorsameSitein two places. Both files persist authentication data with{ expires: 1, path: "/" }and no transport or cross-site restrictions. The token cookie can travel over plaintext HTTP and is sent on top-level cross-site navigations. Define the attributes once and use that constant everywhere.
hooks/useAuth.js#L7-L7: extend the shared constant to{ expires: 1, path: "/", sameSite: "strict", secure: process.env.NODE_ENV === "production" }, and export it so other modules reuse it rather than restating the shape.components/providers/AuthProvider.jsx#L99-L99: import the exported constant and pass it toCookies.set("userInfo", ...)instead of the inline{ expires: 1, path: "/" }literal, so the two writers cannot drift apart.🤖 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 `@hooks/useAuth.js` at line 7, Update the shared COOKIE_OPTIONS constant in hooks/useAuth.js at line 7 to include sameSite: "strict" and production-only secure behavior, then export it. In components/providers/AuthProvider.jsx at line 99, import and reuse COOKIE_OPTIONS for the userInfo cookie instead of the inline options object, ensuring both writers share the same attributes.components/providers/AuthProvider.jsx-103-106 (1)
103-106: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winLog only the error message, not the full error object.
console.logon an Axios error serializeserror.response, and the response body of/api/users/:idis the user record. That can print the email and other identifiers into the browser console. Log the message instead, and useconsole.errorso the level matches the event.Nice work centralizing the session state here, by the way. This is the right place for it.
🔒️ Proposed fix for the log statement
} catch (error) { - console.log("Failed to refresh user:", error); + console.error("Failed to refresh user:", error?.message); toast.error("Failed to refresh user info"); }🤖 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 `@components/providers/AuthProvider.jsx` around lines 103 - 106, Update the catch block in AuthProvider to log only the error message rather than the full Axios error object, and use console.error instead of console.log. Preserve the existing toast notification while preventing response data from being serialized into the browser console.lib/stellar/stellarErrors.js-151-179 (1)
151-179: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winTighten the error classification. Two predicates currently overlap and one swallows real bugs.
Three separate problems here, all from loose substring matching:
error.code === -1matches in bothisNoWalletErrorandisUserRejection. Incomponents/stellar/StellarProvider.jsxline 180 theisNoWalletErrorbranch runs first, so a user who simply closes the wallet modal gets the toast "No Stellar wallet detected. Install Freighter to continue." That is the wrong message for a cancel, and the "silent on rejection" branch at line 192 becomes unreachable for code-1.msg.includes("undefined")andmsg.includes("Cannot read")classify anyTypeErroras "no wallet". A genuine null-dereference bug in the payment path then surfaces as an install prompt. That hides defects from you and misdirects the user. TheselectWalletbug I flagged inhooks/useStellarAuth.jsis exactly this shape.- The matches are case-sensitive. Freighter and Albedo capitalize messages such as "Rejected by user", so those miss.
Drop the generic TypeError patterns, give the modal-closed code to one predicate only, and normalize case once.
🐛 Proposed fix for the predicates
export function isNoWalletError(error) { if (!error) return false; - const msg = typeof error === "string" ? error : error.message || ""; - return ( - msg.includes("no wallet") || - msg.includes("no extension") || - msg.includes("not installed") || - msg.includes("undefined") || - msg.includes("Cannot read") || - error.code === -1 - ); + const msg = (typeof error === "string" ? error : error.message || "").toLowerCase(); + return ( + msg.includes("no wallet") || + msg.includes("no extension") || + msg.includes("not installed") || + msg.includes("wallet not found") + ); } /** * Check if a signing error means the user rejected/cancelled. */ export function isUserRejection(error) { if (!error) return false; - const msg = typeof error === "string" ? error : error.message || ""; + const msg = (typeof error === "string" ? error : error.message || "").toLowerCase(); return ( msg.includes("rejected") || - msg.includes("cancelled") || + msg.includes("cancel") || msg.includes("declined") || msg.includes("denied") || - msg.includes("User declined") || error.code === 4001 || error.code === -1 ); }🤖 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 `@lib/stellar/stellarErrors.js` around lines 151 - 179, Tighten isNoWalletError and isUserRejection: remove the generic "undefined" and "Cannot read" substring checks, assign error.code === -1 to only the predicate representing modal cancellation/user rejection, and eliminate it from the other predicate. Normalize the extracted message once before performing all text comparisons so capitalized wallet messages such as "Rejected by user" are recognized.components/stellar/StellarProvider.jsx-291-300 (1)
291-300: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winDo not compare USDC amounts with
parseFloat.Line 294 compares a balance against a price using IEEE-754 doubles. Stellar amounts carry seven decimal places, and binary floats cannot represent most decimal fractions exactly. A wallet holding exactly the price can compare as short, which blocks a valid purchase, and the reverse can let a payment through that Horizon then rejects with
op_underfunded. The message on line 298 also rounds the balance to two decimals, so a user with10.0000001USDC reads "only have $10.00" next to a $10 price and cannot tell what is wrong.Compare in integer stroops, or use a decimal library.
BigInton the scaled string needs no new dependency.As per path instructions: "Flag any deviation from this flow, any secret-key handling, and any amount arithmetic done with floating point."
🐛 Proposed fix using integer stroops
+// Stellar amounts have 7 decimal places. Compare as integers to avoid float error. +const toStroops = (value) => { + const [whole = "0", frac = ""] = String(value ?? "0").split("."); + return BigInt(whole || "0") * 10_000_000n + BigInt(frac.padEnd(7, "0").slice(0, 7)); +}; + if ( connectedWallet && walletInfo && - parseFloat(walletInfo.usdcBalance || 0) < (price || 0) + toStroops(walletInfo.usdcBalance || 0) < toStroops(price || 0) ) { issues.push({ ...WALLET_ERRORS.insufficient_balance, - message: `You need $${price} USDC but only have $${parseFloat(walletInfo.usdcBalance || 0).toFixed(2)}.`, + message: `You need ${price} USDC but only have ${walletInfo.usdcBalance || 0}.`, }); }🤖 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 `@components/stellar/StellarProvider.jsx` around lines 291 - 300, Update the insufficient-balance check in the wallet validation flow to avoid parseFloat and compare USDC values using exact integer stroops, such as BigInt conversion from scaled decimal strings. Reuse the exact stroop values for both comparison and the user-facing message, preserving sufficient precision instead of rounding the balance to two decimals. Do not introduce secret-key handling or any floating-point amount arithmetic.Source: Path instructions
components/stellar/StellarProvider.jsx-54-89 (1)
54-89: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winUse
@stellar/freighter-api.isConnected()for Freighter detection.The static
StellarWalletsKitAPI is valid in v2.4.0. However,window.freighteris not an object with anisConnected()method. Use the installed@stellar/freighter-apipackage instead, or detection can incorrectly report Freighter as unavailable.🤖 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 `@components/stellar/StellarProvider.jsx` around lines 54 - 89, Update the Freighter detection inside detectWallets to import and call isConnected() from `@stellar/freighter-api` instead of accessing window.freighter.isConnected(). Preserve the existing setHasWalletExtension handling for successful detection and failures, while keeping StellarWalletsKit initialization unchanged.app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx-75-83 (1)
75-83: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
handleResumedoes not remount the player, so the resume position may be ignored.
handleStartOverbumpsplayerKey, buthandleResumeonly setsuseResume. That changeseffectiveStartTimefrom0toresumeTimeand passes it intoVidPlayerBoxasstartTime, which maps toclipStartTimeonMediaPlayer. TheMediaPlayerkeyincomponents/atoms/dashboard/vid-player-box.jsxline 244 depends only onvideo,subtitles, andchapters, so the player instance is reused. A player that has already loaded its source does not necessarily re-apply a changedclipStartTime, so the learner can press Resume and stay at 0.Bump
playerKeyinhandleResumeas well, so both controls behave the same way:🐛 Proposed fix
const handleResume = () => { setUseResume(true); + setPlayerKey((k) => k + 1); };See the separate comment on
clipStartTimeincomponents/atoms/dashboard/vid-player-box.jsx. A seek oncan-playis the better mechanism for resume, and it makes this remount unnecessary.Also applies to: 195-203
🤖 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 `@app/dashboard/courses/`[courseId]/CourseDetailPageClient.jsx around lines 75 - 83, Update handleResume in CourseDetailPageClient to increment playerKey after enabling resume, matching handleStartOver’s remount behavior. Keep setUseResume(true) unchanged so the remounted player receives the resume start time.hooks/useCourseProgress.js-269-311 (1)
269-311: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
loadingnever becomesfalsewhenbackendAvailable === false.The guard at line 270 returns before the
try, so thefinallyblock at line 307 never runs.setLoading(false)andfetchedRef.current = trueare skipped. The hook then returnsloading: trueforever for every learner in a session where the progress endpoint answered 404 or 405 once.
app/dashboard/courses/page.jsxonly destructuresprogressMap, so the defect is currently invisible. Any consumer that gates rendering onloadingwill show a permanent spinner.The
catchpath at line 306 also callsloadAllFromLocal()without checkingcancelled, so it sets state after the effect is torn down.🐛 Proposed fix
async function fetchAll() { if (backendAvailable === false) { - loadAllFromLocal(); - return; + if (!cancelled) { + loadAllFromLocal(); + fetchedRef.current = true; + setLoading(false); + } + return; } @@ } catch (err) { if (err?.response?.status === 404 || err?.response?.status === 405) { backendAvailable = false; } - loadAllFromLocal(); + if (!cancelled) loadAllFromLocal(); } finally { fetchedRef.current = true; if (!cancelled) setLoading(false); } }🤖 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 `@hooks/useCourseProgress.js` around lines 269 - 311, Update fetchAll in useCourseProgress so the backendAvailable === false local-loading path also marks fetchedRef.current and clears loading before returning, while preserving cancellation safety. In the catch path, guard loadAllFromLocal with cancelled so teardown cannot update state, and ensure the existing finally behavior remains applied to normal requests.hooks/useCourseProgress.js-161-195 (1)
161-195: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winThrottle the state update and the
localStoragewrite.
reportProgressruns on every Vidstacktime-updateevent, which fires several times per second. Each call does two expensive things:
setProgresswith a fresh object, so the whole course detail page re-renders while the video plays.JSON.stringifyplus a synchronouslocalStorage.setItem, which blocks the main thread on the same cadence.Only the backend write is throttled today. The visible progress bar needs whole-percent granularity, so most of these updates produce no user-visible change.
⚡ Proposed fix
+ const lastLocalWriteRef = useRef(0); + const reportProgress = useCallback( (positionSeconds, durationSeconds) => { @@ currentDataRef.current = data; - setProgress({ - percent: pct, - positionSeconds, - durationSeconds, - completed: done, - }); - - writeLocalProgress(userId, courseId, data); - const now = Date.now(); + + setProgress((prev) => + prev.percent === pct && prev.completed === done + ? prev + : { percent: pct, positionSeconds, durationSeconds, completed: done } + ); + + if (now - lastLocalWriteRef.current >= LOCAL_WRITE_MS) { + lastLocalWriteRef.current = now; + writeLocalProgress(userId, courseId, data); + } + if (now - lastWriteRef.current >= THROTTLE_MS) {Add a constant near line 7, for example
const LOCAL_WRITE_MS = 3000;, and keep the existingflushNowcall in the unmount cleanup so the final position is still persisted.Note that
setProgresswith a bailout keepspositionSecondsslightly stale between percent changes. The time readout atCourseDetailPageClient.jsxline 209 then advances in steps. If you want a smooth readout, keep the position in a ref and update state on a fixed interval instead.🤖 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 `@hooks/useCourseProgress.js` around lines 161 - 195, Throttle both the visible state update and localStorage persistence in reportProgress: update setProgress only when the calculated whole percent changes, and gate writeLocalProgress with a separate LOCAL_WRITE_MS interval (for example, 3000ms). Track the latest progress data in currentDataRef, and preserve the existing flushNow unmount cleanup so the final position is persisted.hooks/useCourseProgress.js-76-139 (1)
76-139: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd a cancellation flag to this effect.
The effect depends on
courseId, andfetchFromApicallssetProgress,setLoading, and the refs afterawait. If a learner navigates from course A to course B before the first request resolves, the stale response for course A writes into the state and refs of the mount for course B. The player then shows the wrong resume position, andcompletedRefcan be wrongly set totrue.
useAllCourseProgressat line 267 already uses this pattern, so applying it here keeps both hooks consistent.🛡️ Proposed fix
useEffect(() => { if (!userId || !courseId) { setLoading(false); return; } + let cancelled = false; + const local = readLocalProgress(userId, courseId); @@ async function fetchFromApi() { if (backendAvailable === false) { - setLoading(false); + if (!cancelled) setLoading(false); return; } try { const res = await axiosInstance.get("/api/progress/courses"); + if (cancelled) return; if (res.data?.success && Array.isArray(res.data.progress)) { @@ } finally { - setLoading(false); - apiCheckedRef.current = true; + if (!cancelled) { + setLoading(false); + apiCheckedRef.current = true; + } } } fetchFromApi(); + + return () => { + cancelled = true; + }; }, [userId, courseId]);🤖 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 `@hooks/useCourseProgress.js` around lines 76 - 139, Add a cancellation flag inside the useEffect and set it when the effect cleanup runs. In fetchFromApi, after the await and before applying the response, updating progress, loading state, or refs, return early when the effect has been cancelled; also guard the finally updates so stale requests cannot affect the current course. Keep the existing useAllCourseProgress cancellation pattern consistent.Source: Linters/SAST tools
app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx-69-73 (1)
69-73: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
handleEndedcannot mark completion whendurationSecondsis 0.
handleEndedreportsprogress.durationSecondsfor both the position and the duration. If the value is still0, the call becomesreportProgress(0, 0).isCompletedreturnsfalsefor a zero duration, so the course is never marked complete. This happens for a short video that ends before the first throttled state update lands, and afterhandleStartOver, which resetsdurationSecondsto0.The player already knows the real duration. Forward it from the
endedevent instead of reading it from state.🐛 Proposed fix
In
components/atoms/dashboard/vid-player-box.jsx, pass the duration onended:const endedSub = player.on('ended', () => { if (onEnded) { - onEnded(); + onEnded(player.state.duration); } });Then use it here:
- const handleEnded = useCallback(() => { - if (course?._id) { - reportProgress(progress.durationSeconds || 0, progress.durationSeconds || 0); - } - }, [reportProgress, progress.durationSeconds, course?._id]); + const handleEnded = useCallback( + (duration) => { + const total = duration || progress.durationSeconds || 0; + if (!course?._id || total <= 0) return; + reportProgress(total, total); + }, + [reportProgress, progress.durationSeconds, course?._id] + );🤖 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 `@app/dashboard/courses/`[courseId]/CourseDetailPageClient.jsx around lines 69 - 73, Update the video player’s ended-event handling in VidPlayerBox to pass the player’s actual duration, then change CourseDetailPageClient’s handleEnded to accept and forward that duration for both reportProgress arguments instead of using progress.durationSeconds.components/organisms/dashboard/UpcomingSessions.jsx-18-34 (1)
18-34: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDistinguish a fetch failure from an empty list.
The
catchblock setssessionsto[], which renders the same "No upcoming sessions" empty state as a real fetch failure. A user cannot tell a backend outage from having no sessions, and this hides the failure from anyone watching for errors.🔧 Proposed fix to surface fetch failures separately
const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); useEffect(() => { let mounted = true; const fetch = async () => { try { const data = await getSpaces(); if (!mounted) return; const upcoming = (data || []).filter(s => s.status === "upcoming"); setSessions(upcoming); + setError(false); } catch { - if (mounted) setSessions([]); + if (mounted) { + setSessions([]); + setError(true); + } } finally { if (mounted) setLoading(false); } }; fetch(); return () => { mounted = false; }; }, []);Then render a distinct message when
erroris true, instead of falling through to the empty-state branch.Also applies to: 51-55
🤖 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 `@components/organisms/dashboard/UpcomingSessions.jsx` around lines 18 - 34, Update the UpcomingSessions useEffect and render logic to track fetch failure separately from the sessions list: set an error state in the catch path instead of treating failure as an empty result, and render a distinct failure message when that state is true before the “No upcoming sessions” branch. Preserve the existing empty-state behavior for successful fetches returning no upcoming sessions.components/organisms/dashboard/StatsOverview.jsx-53-79 (1)
53-79: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winThe Retry button does not retry anything.
setRetryKeyonly updates local state. That state is read by thekeyprop on the success-branch container at Line 82, which is unreachable whileerrorstays true.useStats(hooks/useStats.js) only re-runs its fetch effect whenuser?._idchanges; it never re-fetches in response toretryKey. After a real failure, the user is stuck on the error screen: clicking "Retry" changes nothing, and the widget never recovers without a full page reload.Expose a way to re-trigger the fetch from
useStats, and call it from the Retry button.🔧 Proposed fix: wire Retry to an actual refetch
- <Button round className="bg-accent text-white text-sm" onClick={() => setRetryKey(k => k + 1)}> + <Button round className="bg-accent text-white text-sm" onClick={() => { setRetryKey(k => k + 1); refetch(); }}> Retry </Button>- const { coursesEnrolled, booksRead, upcomingSessions, messagesUnread, totalUptime, loading, error } = useStats(); + const { coursesEnrolled, booksRead, upcomingSessions, messagesUnread, totalUptime, loading, error, refetch } = useStats();
hooks/useStats.jsneeds to exposerefetch(outside the reviewed range):export default function useStats() { const { user } = useAuth(); const [stats, setStats] = useState({ /* ... */ loading: true, error: null }); const fetchStats = useCallback(async () => { if (!user?._id) return; try { const res = await axiosInstance.get(`/api/users/${user._id}/stats`); setStats({ ...res.data, loading: false, error: null }); } catch (error) { setStats((prev) => ({ ...prev, loading: false, error: error.message || "Failed to fetch stats" })); } }, [user?._id]); useEffect(() => { setStats((prev) => ({ ...prev, loading: true })); fetchStats(); }, [fetchStats]); return { ...stats, refetch: fetchStats }; }Also applies to: 81-88
🤖 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 `@components/organisms/dashboard/StatsOverview.jsx` around lines 53 - 79, Expose a refetch function from useStats by reusing its fetch routine, then destructure it in StatsOverview and invoke it from the Retry button alongside any existing retry-state update. Ensure the fetch routine remains reactive to user?._id and the button triggers a new request while the error view is displayed.components/organisms/dashboard/UpcomingSessions.jsx-68-69 (1)
68-69: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winGuard against unparseable
eventDatevalues before formatting.If
s.eventDateis truthy but invalid,date-fnsv3.6.0format()throwsRangeError: Invalid time valueduring render. UseisValid()before formatting, preserve the truthy check, and reuse the parsedDate.🤖 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 `@components/organisms/dashboard/UpcomingSessions.jsx` around lines 68 - 69, Update the event date rendering around the two spans to parse s.eventDate once, preserve the existing truthy check, and use date-fns isValid() to verify the parsed Date before calling format(). Render empty strings for missing or invalid dates, while reusing the validated Date for both date and time formats.components/organisms/dashboard/CommandPalette.jsx-40-46 (1)
40-46: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win"reel" results never render in the palette.
typeLabels(Lines 40-46),typeLinks(Lines 48-54), andgetTypeIcon(Lines 56-71) all define a "reel" type, andgroupedResults(Lines 247-265) buckets it. But the rendered sections (Lines 310-430) only covercourse,book,user, andspace. If the backend returns "reel" results,results.length > 0is true, so the "No results found" empty state (Line 306-308) never shows either — matching reel items simply vanish from the UI.Add a "Reels"
CommandGroup, mirroring the existing pattern, or better, replace the four near-identical blocks with one config-driven loop over[{ type: "course", heading: "Courses" }, ...](also notetypeLabelsat Lines 40-46 is currently unused — the headings are hardcoded strings — so it could drive this loop directly).Also applies to: 310-430
🤖 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 `@components/organisms/dashboard/CommandPalette.jsx` around lines 40 - 46, Update the rendered command sections in the CommandPalette component to include groupedResults.reel, adding a “Reels” CommandGroup that follows the existing course, book, user, and space rendering pattern. Prefer reusing typeLabels or a shared configuration to avoid duplicated blocks, while preserving existing headings, links, icons, and item rendering for all result types.components/organisms/dashboard/CommandPalette.jsx-86-89 (1)
86-89: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winRoute "Open Wallet" to
/account/wallet.
/account/walletis the dedicated Stellar wallet-management route./dashboard/earningsis the earnings analytics route and is already used by"Earnings".🤖 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 `@components/organisms/dashboard/CommandPalette.jsx` around lines 86 - 89, Update the "Open Wallet" entry in quickActions to use the dedicated /account/wallet route instead of /dashboard/earnings; leave the existing label and icon unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 32c92ef8-8342-4d58-b714-bacc1db68802
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (74)
.env.exampleREADME.mdapp/(pages)/educators/[profileid]/EducatorPageClient.jsxapp/(pages)/educators/[profileid]/page.jsxapp/account/profile/[profileid]/page.jsxapp/account/wallet/page.jsxapp/api/ai/chat/route.jsapp/api/ai/stream/route.jsapp/api/books/[bookId]/preview/route.jsapp/dashboard/courses/[courseId]/CourseDetailPageClient.jsxapp/dashboard/courses/[courseId]/page.jsxapp/dashboard/courses/page.jsxapp/dashboard/layout.jsxapp/dashboard/library/[bookid]/page.jsxapp/dashboard/library/page.jsxapp/dashboard/library/read/[bookid]/page.jsxapp/dashboard/search/[searchparam]/page.jsxapp/dashboard/spaces/[spacesid]/page.jsxapp/layout.jscomponents/DebugAuthLogs.jsxcomponents/atoms/dashboard/Notybell.jsxcomponents/atoms/dashboard/Searchbox.jsxcomponents/atoms/dashboard/vid-player-box.jsxcomponents/atoms/form/ComboBox.jsxcomponents/atoms/reels/ReelActionButton.jsxcomponents/molecules/Modal.jscomponents/molecules/dashboard/LibraryToolbar.jsxcomponents/molecules/dashboard/Notybell.jsxcomponents/molecules/dashboard/cards/courseCard.jsxcomponents/molecules/dashboard/cards/educators/PublicBookCard.jsxcomponents/molecules/dashboard/cards/educators/PublicCourseCard.jsxcomponents/molecules/dashboard/cards/educators/PublicSpaceCard.jsxcomponents/molecules/dashboard/cards/libraryCard.jsxcomponents/molecules/dashboard/cards/spaceCard.jsxcomponents/molecules/dashboard/nav-header.jsxcomponents/molecules/errors/NotFound.jsxcomponents/organisms/auth/login-form.jsxcomponents/organisms/auth/signup-form.jsxcomponents/organisms/create/book-create-form.jsxcomponents/organisms/create/course-create-form.jsxcomponents/organisms/create/space-create-form.jsxcomponents/organisms/dashboard/CommandPalette.jsxcomponents/organisms/dashboard/JaasMeetingClientSection.jsxcomponents/organisms/dashboard/LearningProgress.jsxcomponents/organisms/dashboard/StatsOverview.jsxcomponents/organisms/dashboard/UpcomingSessions.jsxcomponents/organisms/dashboard/ai/Ai-Sidebar.jsxcomponents/organisms/educators/EducatorProfileHeader.jsxcomponents/organisms/reels/ReelCard.jsxcomponents/organisms/reels/ReelFeed.jsxcomponents/providers/AuthProvider.jsxcomponents/stellar/PaymentModal.jsxcomponents/stellar/StellarProvider.jsxcomponents/stellar/TransactionHistory.jsxcomponents/stellar/WalletConnectButton.jsxcomponents/stellar/WalletOnboarding.jsxcomponents/ui/command.jsxhooks/useAuth.jshooks/useCourseProgress.jshooks/useDebouncedValue.jshooks/useSearch.jshooks/useStellarAuth.jshooks/useStellarPayment.jshooks/useWalletReadiness.jslib/actions/ai/load-chat-history.jslib/actions/cached-api.jslib/config/axios.config.jslib/config/env.jslib/config/firebase.config.jslib/data.jslib/search-fallback.jslib/stellar/stellarErrors.jslib/utils/cloudinaryUpload.jspackage.json
💤 Files with no reviewable changes (6)
- components/organisms/reels/ReelFeed.jsx
- hooks/useWalletReadiness.js
- package.json
- components/DebugAuthLogs.jsx
- components/stellar/WalletOnboarding.jsx
- lib/data.js
| <<<<<<< HEAD | ||
| <ThemeProvider> | ||
| <AppearanceProvider> | ||
| <CacheProvider> | ||
| <StellarProvider>{children}</StellarProvider> | ||
| <AuthProvider> | ||
| <StellarProvider>{children}</StellarProvider> | ||
| </AuthProvider> | ||
| </CacheProvider> | ||
| <Toaster position="top-right" /> | ||
| </AppearanceProvider> | ||
| </ThemeProvider> | ||
| >>>>>>> origin/dev |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Remove the unresolved merge conflict markers.
Lines 76 and 87 still contain <<<<<<< HEAD and >>>>>>> origin/dev. JSX cannot parse these tokens, so next build and npm run lint both fail. Biome reports parse errors at the same lines. The provider nesting itself is correct, since StellarProvider calls useAuth, so it must stay inside AuthProvider. Only the markers need to go.
🐛 Proposed fix to remove the conflict markers
-<<<<<<< HEAD
<ThemeProvider>
<AppearanceProvider>
<CacheProvider>
<AuthProvider>
<StellarProvider>{children}</StellarProvider>
</AuthProvider>
</CacheProvider>
<Toaster position="top-right" />
</AppearanceProvider>
</ThemeProvider>
->>>>>>> origin/dev📝 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.
| <<<<<<< HEAD | |
| <ThemeProvider> | |
| <AppearanceProvider> | |
| <CacheProvider> | |
| <StellarProvider>{children}</StellarProvider> | |
| <AuthProvider> | |
| <StellarProvider>{children}</StellarProvider> | |
| </AuthProvider> | |
| </CacheProvider> | |
| <Toaster position="top-right" /> | |
| </AppearanceProvider> | |
| </ThemeProvider> | |
| >>>>>>> origin/dev | |
| <ThemeProvider> | |
| <AppearanceProvider> | |
| <CacheProvider> | |
| <AuthProvider> | |
| <StellarProvider>{children}</StellarProvider> | |
| </AuthProvider> | |
| </CacheProvider> | |
| <Toaster position="top-right" /> | |
| </AppearanceProvider> | |
| </ThemeProvider> |
🧰 Tools
🪛 Biome (2.5.5)
[error] 76-76: Expected a JSX Expression, a Element, or a text but instead found '<<<<<<'.
(parse)
[error] 77-77: expected > but instead found <
(parse)
[error] 76-76: Expected corresponding JSX closing tag for 'HEAD'.
(parse)
[error] 87-87: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 87-87: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 87-87: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 87-87: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 87-87: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 87-87: Unexpected token. Did you mean {'>'} or >?
(parse)
[error] 87-87: Unexpected token. Did you mean {'>'} or >?
(parse)
🪛 GitHub Actions: CI / 0_Lint and Build.txt
[error] 76-76: ESLint parsing error: Unexpected token at line 76. The 'npm run lint' command failed with exit code 1.
🪛 GitHub Actions: CI / Lint and Build
[error] 76-76: ESLint parsing error: Unexpected token at line 76. The 'npm run lint' command failed with exit code 1.
🤖 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 `@app/layout.js` around lines 76 - 87, Remove the unresolved merge conflict
markers surrounding the provider JSX in the layout component, while preserving
the existing ThemeProvider, AppearanceProvider, CacheProvider, AuthProvider,
StellarProvider, and Toaster structure. Keep StellarProvider nested inside
AuthProvider and do not alter the provider behavior.
Source: Linters/SAST tools
| <MediaPlayer | ||
| src={data?.video} | ||
| key={JSON.stringify({ video: data.video, subtitles: data.subtitles, chapters: data.chapters })} | ||
| src={data.video} | ||
| viewType='video' | ||
| streamType='on-demand' | ||
| logLevel='warn' | ||
| playsInline | ||
| title={data?.title} | ||
| poster={data?.thumbnail} | ||
| clipStartTime={startTime || undefined} | ||
| storage={playbackPreferencesStorage} | ||
| onError={() => setHasPlaybackError(true)} | ||
| tabIndex={0} | ||
| aria-label={data?.title ? `Video player: ${data.title}` : 'Video player'} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Resume is built on a clip window instead of a seek. The saved position flows from useCourseProgress into MediaPlayer as clipStartTime, which redefines the playable range rather than seeking. That single decision produces both the unreachable early section and the rebased currentTime/duration that corrupt the recorded percentage. It also forces the caller to remount the player to apply a new position.
components/atoms/dashboard/vid-player-box.jsx#L243-L256: removeclipStartTime, acceptstartTimeinPlayerProgressTracker, and seek once on thecan-playevent withplayer.remoteControl.seek(startTime).app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx#L75-L83: once the player seeks oncan-play, keephandleResumeas a plainsetUseResume(true). Until then, bumpplayerKeythere so the newstartTimeactually reaches a fresh player instance.
📍 Affects 2 files
components/atoms/dashboard/vid-player-box.jsx#L243-L256(this comment)app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx#L75-L83
🤖 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 `@components/atoms/dashboard/vid-player-box.jsx` around lines 243 - 256, The
resume flow must seek to the saved position instead of using clipStartTime. In
components/atoms/dashboard/vid-player-box.jsx lines 243-256, remove
clipStartTime, pass startTime into PlayerProgressTracker, and seek once on
can-play via player.remoteControl.seek(startTime). In
app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx lines 75-83, keep
handleResume as a plain setUseResume(true) and increment playerKey there so the
updated startTime reaches a fresh player instance.
| <Controller control={form.control} name="role" render={({ field }) => ( | ||
| <FormItem> | ||
| <FormLabel>Role</FormLabel> | ||
| <FormControl> | ||
| <Select value={field.value} onValueChange={field.onChange}> | ||
| <SelectTrigger className="w-[180px]"> | ||
| <SelectValue placeholder="Role" /> | ||
| </SelectTrigger> | ||
| <SelectContent> | ||
| <SelectItem value="student">Student</SelectItem> | ||
| <SelectItem value="tutor">Tutor</SelectItem> | ||
| <SelectItem value="admin">Admin</SelectItem> | ||
| </SelectContent> | ||
| </Select> | ||
| </FormControl> | ||
| <FormMessage /> | ||
| </FormItem> | ||
| )} /> |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Form primitives are used without FormFieldContext in four places. FormItem, FormLabel, FormControl, and FormMessage all call useFormField(), and only FormField provides FormFieldContext (components/ui/form.jsx Lines 13-23). A bare Controller, or a FormLabel placed outside any field, leaves that context empty. The result is a missing field name, no aria-invalid wiring, no label association, and no validation message, and useFormField() can throw.
components/organisms/auth/signup-form.jsx#L150-L167: change theroleControllertoFormField.components/organisms/create/book-create-form.jsx#L113-L123: replace both standaloneFormLabelelements withLabeland addhtmlFor="book-thumbnail"andhtmlFor="book-file".components/organisms/create/course-create-form.jsx#L128-L136: change thecategoryControllertoFormField, and replace the standaloneFormLabelelements on Lines 146 and 157 withLabelplushtmlFor.components/organisms/create/space-create-form.jsx#L118-L134: change theeventDateandeventTimeControllerelements toFormField, and replace the standaloneFormLabelon Line 145 withLabel htmlFor="space-thumbnail".
📍 Affects 4 files
components/organisms/auth/signup-form.jsx#L150-L167(this comment)components/organisms/create/book-create-form.jsx#L113-L123components/organisms/create/course-create-form.jsx#L128-L136components/organisms/create/space-create-form.jsx#L118-L134
🤖 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 `@components/organisms/auth/signup-form.jsx` around lines 150 - 167, Wrap the
role Controller in signup-form.jsx (150-167) with FormField so the form
primitives receive FormFieldContext. Apply the same Controller-to-FormField
change to eventDate and eventTime in
components/organisms/create/space-create-form.jsx (118-134) and category in
components/organisms/create/course-create-form.jsx (128-136). In
components/organisms/create/book-create-form.jsx (113-123), replace standalone
FormLabel elements with Label using htmlFor="book-thumbnail" and
htmlFor="book-file"; in course-create-form.jsx (128-136), replace the standalone
labels at lines 146 and 157 with Label and matching htmlFor values; in
space-create-form.jsx (118-134), replace the standalone thumbnail label at line
145 with Label htmlFor="space-thumbnail".
| const data = await createCourse({ | ||
| form, | ||
| thumbnailUrl, | ||
| videoUrl, | ||
| category: form.category, | ||
| category: data.category, | ||
| }); | ||
|
|
||
| if (data && data.success) { | ||
| if (result && result.success) { | ||
| toast.success("Course created successfully!"); | ||
| router.push(`/dashboard/courses/${data.course._id}`); | ||
| router.push(`/dashboard/courses/${result.course._id}`); | ||
| } else { | ||
| toast.error(data?.message || "Failed to create course."); | ||
| toast.error(result?.message || "Failed to create course."); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect the createCourse signature and payload handling.
fd -t f 'create-course.js' | xargs -r cat -nRepository: Deen-Bridge/dnb-frontend
Length of output: 1053
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- target form ---'
cat -n components/organisms/create/course-create-form.jsx | sed -n '1,150p'
printf '%s\n' '--- createCourse call sites ---'
rg -n -C 8 'createCourse\s*\(' --glob '*.{js,jsx,ts,tsx}' .
printf '%s\n' '--- createCourse implementation ---'
fd -t f 'create-course.js' | while IFS= read -r file; do
printf '\n--- %s ---\n' "$file"
cat -n "$file"
doneRepository: Deen-Bridge/dnb-frontend
Length of output: 11812
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- course response producers ---'
rg -n -C 6 'createCourse|/api/courses|Course created|success.*course|course.*success' \
--glob '*.{js,jsx,ts,tsx}' \
--glob '!node_modules' .
printf '%s\n' '--- standalone JavaScript scope check ---'
node - <<'JS'
function broken(onCreate) {
const data = { category: "submitted" };
try {
const data = onCreate({ category: data.category });
return data;
} catch (error) {
return `${error.name}: ${error.message}`;
}
}
function fixed(onCreate) {
const data = { category: "submitted" };
try {
const result = onCreate({ form: data, category: data.category });
return result;
} catch (error) {
return `${error.name}: ${error.message}`;
}
}
console.log("broken:", broken((payload) => payload));
console.log("fixed:", fixed((payload) => ({ success: true, payload })));
try {
console.log(result);
} catch (error) {
console.log("undeclared result:", `${error.name}: ${error.message}`);
}
JSRepository: Deen-Bridge/dnb-frontend
Length of output: 27365
Fix the onSubmit payload and result binding.
The local data declaration shadows the submitted data parameter. data.category then throws ReferenceError: Cannot access 'data' before initialization. result is also undeclared. Pass form: data because createCourse reads form.title, form.description, and form.price from submitted values. Store the return value as result.
🤖 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 `@components/organisms/create/course-create-form.jsx` around lines 88 - 100,
Update the onSubmit handler to avoid shadowing its submitted data parameter:
pass that parameter as form when calling createCourse, and bind the returned
value to result before checking success and navigating or displaying the error
toast.
| <form | ||
| onSubmit={handleSubmit} | ||
| className="w-sm sm:w-lg mx-auto rounded-xl p-1 space-y-3" | ||
| > | ||
| <Label htmlFor="title">Course title</Label> | ||
| <Input | ||
| id="title" | ||
| name="title" | ||
| placeholder="Course Title" | ||
| value={form.title} | ||
| onChange={handleChange} | ||
| required | ||
| /> | ||
| <Label htmlFor="title">Course description</Label> | ||
| <Label htmlFor="description">Course description</Label> | ||
| <Textarea | ||
| id="description" | ||
| name="description" | ||
| placeholder="Book Description" | ||
| value={form.description} | ||
| onChange={handleChange} | ||
| required | ||
| className="w-full h-24 resize-none overflow-y-auto" | ||
| /> | ||
| <Label htmlFor="title">Course Category</Label> | ||
| <Label htmlFor="category">Course Category</Label> | ||
| <CategoryCombobox | ||
| id="category" | ||
| category={form.category} | ||
| setCategory={(value) => | ||
| setForm((prev) => ({ ...prev, category: value })) | ||
| } | ||
| /> | ||
| <Label htmlFor="title">Course price</Label> | ||
| <Label htmlFor="price">Course price</Label> | ||
| <Input | ||
| id="price" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
The legacy form block breaks the build. Delete it.
The old markup remains after </Form>. Two sibling JSX elements are returned without a wrapper, so the file does not parse; Biome reports expected ")" but instead found "onSubmit" at Line 185 and many follow-on parse errors. The block also references identifiers that no longer exist in this component: handleSubmit, handleChange, setForm, loading, Label, and form.title/form.description/form.category/form.price as plain state. CI cannot pass in this state.
🐛 Proposed fix: remove Lines 184-280
</form>
</Form>
- <form
- onSubmit={handleSubmit}
- className="w-sm sm:w-lg mx-auto rounded-xl p-1 space-y-3"
- >
- <Label htmlFor="title">Course title</Label>
- ... (delete the whole legacy form through its closing </form>)
- </form>
);
};📝 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.
| <form | |
| onSubmit={handleSubmit} | |
| className="w-sm sm:w-lg mx-auto rounded-xl p-1 space-y-3" | |
| > | |
| <Label htmlFor="title">Course title</Label> | |
| <Input | |
| id="title" | |
| name="title" | |
| placeholder="Course Title" | |
| value={form.title} | |
| onChange={handleChange} | |
| required | |
| /> | |
| <Label htmlFor="title">Course description</Label> | |
| <Label htmlFor="description">Course description</Label> | |
| <Textarea | |
| id="description" | |
| name="description" | |
| placeholder="Book Description" | |
| value={form.description} | |
| onChange={handleChange} | |
| required | |
| className="w-full h-24 resize-none overflow-y-auto" | |
| /> | |
| <Label htmlFor="title">Course Category</Label> | |
| <Label htmlFor="category">Course Category</Label> | |
| <CategoryCombobox | |
| id="category" | |
| category={form.category} | |
| setCategory={(value) => | |
| setForm((prev) => ({ ...prev, category: value })) | |
| } | |
| /> | |
| <Label htmlFor="title">Course price</Label> | |
| <Label htmlFor="price">Course price</Label> | |
| <Input | |
| id="price" |
🧰 Tools
🪛 Biome (2.5.5)
[error] 185-185: expected ) but instead found onSubmit
(parse)
[error] 193-193: expected , but instead found .
(parse)
[error] 196-196: Expected an expression but instead found '>'.
(parse)
[error] 202-202: expected , but instead found .
(parse)
[error] 206-206: Expected an expression but instead found '>'.
(parse)
[error] 210-210: expected , but instead found .
(parse)
[error] 211-211: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '('.
(parse)
[error] 211-211: Expected a function body but instead found '=>'.
(parse)
[error] 212-212: expected , but instead found setForm
(parse)
[error] 212-212: Expected a parameter but instead found '('.
(parse)
[error] 212-212: expected , but instead found prev
(parse)
[error] 212-212: Expected a function body but instead found '=>'.
(parse)
[error] 212-212: Expected a semicolon or an implicit semicolon after a statement, but found none
(parse)
[error] 214-214: unterminated regex literal
(parse)
[error] 215-215: Expected a semicolon or an implicit semicolon after a statement, but found none
(parse)
[error] 215-215: Expected a semicolon or an implicit semicolon after a statement, but found none
(parse)
[error] 215-215: Expected a semicolon or an implicit semicolon after a statement, but found none
(parse)
🤖 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 `@components/organisms/create/course-create-form.jsx` around lines 184 - 217,
Delete the entire legacy form JSX block beginning with the extra <form
onSubmit={handleSubmit}> after the existing </Form> and ending at its closing
tag. Preserve the current Form implementation and remove all references from
this obsolete block, including handleSubmit, handleChange, setForm, loading,
Label, and the plain form state fields.
Source: Linters/SAST tools
| const { initializePayment, executePayment, isProcessing } = | ||
| useStellarPayment(); | ||
|
|
||
| const [step, setStep] = useState("preview"); // preview | confirm | processing | success | error | ||
| const [step, setStep] = useState("preview"); | ||
| const [paymentData, setPaymentData] = useState(null); | ||
| const [result, setResult] = useState(null); | ||
| const [error, setError] = useState(null); | ||
| const [errorDetail, setErrorDetail] = useState(null); | ||
| const [showQr, setShowQr] = useState(false); | ||
| const [showWizard, setShowWizard] = useState(false); | ||
| const [preCheckIssues, setPreCheckIssues] = useState([]); | ||
|
|
||
| // Reset state when modal opens/closes | ||
| useEffect(() => { | ||
| if (isOpen) { | ||
| closingRef.current = false; |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
Two identifiers are used but never declared. The modal throws on open and on close.
-
Line 62 assigns
closingRef.current, and lines 133-134 read and write it, but nouseRefcall createsclosingRef.useRefis imported on line 2 and never called. The reset effect runs as soon asisOpenturns true, so opening the modal throwsReferenceError: closingRef is not definedand React unmounts the tree. The purchase flow never renders. -
Lines 137 and 151 call
cancelPayment(), but line 49 destructures onlyinitializePayment,executePayment, andisProcessingfromuseStellarPayment().cancelPaymentisundefined, so closing the modal with a pendingpaymentDatathrowsTypeError: cancelPayment is not a function. The pending transaction is then never released on the backend.
Declare the ref and pull cancelPayment out of the hook. The hook already exports it.
🐛 Proposed fix
- const { initializePayment, executePayment, isProcessing } =
+ const { initializePayment, executePayment, cancelPayment, isProcessing } =
useStellarPayment();
const [step, setStep] = useState("preview");
const [paymentData, setPaymentData] = useState(null);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [errorDetail, setErrorDetail] = useState(null);
const [showQr, setShowQr] = useState(false);
const [preCheckIssues, setPreCheckIssues] = useState([]);
+ const closingRef = useRef(false);Also applies to: 132-154
🤖 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 `@components/stellar/PaymentModal.jsx` around lines 49 - 62, Update
PaymentModal’s useStellarPayment destructuring to include cancelPayment, and
initialize the existing closingRef with useRef before the effects that access
it. Preserve the current reset and cancellation flows so opening the modal no
longer throws and pending payments are released when closing or cancelling.
| // Check network alignment when wallet connects | ||
| useEffect(() => { | ||
| if (!connectedWallet || !kitInitialized) return; | ||
|
|
||
| async function checkNetwork() { | ||
| try { | ||
| const { address } = await StellarWalletsKit.authModal().catch(() => ({})); | ||
| // If we can reach this, wallet is available. | ||
| // The kit was initialized with the correct network, so if auth succeeds | ||
| // the wallet should be on the right network. | ||
| setNetworkMismatch(false); | ||
| setWalletNetwork(NETWORK); | ||
| } catch (error) { | ||
| if (!isNoWalletError(error)) { | ||
| // Some other issue — could be network mismatch | ||
| const msg = error?.message || ""; | ||
| if (msg.includes("network") || msg.includes("mismatch")) { | ||
| setNetworkMismatch(true); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| checkNetwork(); | ||
| }, [connectedWallet, kitInitialized]); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
This effect opens a wallet modal on page load. Remove the authModal() call.
StellarWalletsKit.authModal() is an interactive prompt. This effect runs whenever connectedWallet becomes truthy, and connectedWallet is restored from /api/stellar/wallet/me on every page load for any user with a saved wallet. So a returning user lands on the dashboard and a wallet selection modal appears with no action from them. Users read that as a phishing attempt, and it also races the real connectWallet modal.
The effect also cannot detect what it claims to. The comment on lines 122-124 concedes that a successful auth only proves the wallet is reachable, so networkMismatch is set from substring matching on an error message that a passing call never produces. The destructured address on line 121 is unused.
Two further defects in the same block: .catch(() => ({})) on line 121 swallows the error, so the catch on line 127 is unreachable for anything authModal throws. And per React Doctor, setNetworkMismatch/setWalletNetwork run after await with no cancellation guard, so overlapping runs can write stale values.
Read the network from the wallet without prompting. If the kit exposes getNetwork(), compare its passphrase against EXPECTED_PASSPHRASE. Otherwise drop the effect and rely on the signing failure, which already reports a network mismatch through mapStellarError.
🐛 Proposed direction: compare the network without an interactive prompt
// Check network alignment when wallet connects
useEffect(() => {
if (!connectedWallet || !kitInitialized) return;
+ let cancelled = false;
async function checkNetwork() {
try {
- const { address } = await StellarWalletsKit.authModal().catch(() => ({}));
- // If we can reach this, wallet is available.
- // The kit was initialized with the correct network, so if auth succeeds
- // the wallet should be on the right network.
- setNetworkMismatch(false);
- setWalletNetwork(NETWORK);
- } catch (error) {
- if (!isNoWalletError(error)) {
- // Some other issue — could be network mismatch
- const msg = error?.message || "";
- if (msg.includes("network") || msg.includes("mismatch")) {
- setNetworkMismatch(true);
- }
- }
+ // Non-interactive read. Adjust to the kit's actual accessor.
+ const { networkPassphrase } = await StellarWalletsKit.getNetwork();
+ if (cancelled) return;
+ setNetworkMismatch(networkPassphrase !== EXPECTED_PASSPHRASE);
+ setWalletNetwork(networkPassphrase === Networks.PUBLIC ? "mainnet" : "testnet");
+ } catch {
+ if (cancelled) return;
+ // Unknown network: do not block the user on a failed probe.
+ setNetworkMismatch(false);
}
}
checkNetwork();
+ return () => {
+ cancelled = true;
+ };
}, [connectedWallet, kitInitialized]);🧰 Tools
🪛 ast-grep (0.45.0)
[warning] 125-125: Avoid using the initial state variable in setState
Context: setWalletNetwork(NETWORK)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.
(setstate-same-var)
🪛 React Doctor (0.9.1)
[error] 116-116: This setter runs after await, so overlapping re-runs of the effect can resolve out of order and write stale state; gate it behind a cancellation/ignore flag or return a cleanup that cancels the work.
In a useEffect whose dependencies can change, guard any setter call that runs after an await behind a cancellation/ignore flag, or return a cleanup that cancels the async work.
(no-set-state-after-await-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 `@components/stellar/StellarProvider.jsx` around lines 115 - 139, Replace the
authModal-based logic in the network-alignment effect with a non-interactive
network read using StellarWalletsKit.getNetwork(), comparing its passphrase with
EXPECTED_PASSPHRASE and updating network state only while the effect is still
active. Remove the unused address and swallowed-error handling; if getNetwork()
is unavailable, remove this effect and rely on signing failures handled by
mapStellarError.
Source: Linters/SAST tools
| toast.error(error.message || "Failed to connect wallet"); | ||
| } | ||
| } finally { | ||
| setIsConnecting(false); | ||
| } | ||
| }, [kitInitialized, user, refreshUser]); | ||
| }, [kitInitialized, user, refreshUser, selectWallet]); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
selectWallet is referenced in two files but defined in neither. StellarProvider never declares a selectWallet function and never puts one in its context value, yet both files depend on it. The provider crashes on render, and Sign in with Stellar cannot obtain an address. One definition fixes both sites.
components/stellar/StellarProvider.jsx#L196-L201: define aselectWalletcallback that opens the wallet picker and returns the chosen address, then add it to thevalueobject at lines 307-323. Until it exists, remove it from this dependency array, because evaluating the array throwsReferenceError: selectWallet is not definedand takes down every page under the provider.hooks/useStellarAuth.js#L71-L72: after the provider exportsselectWallet, this destructure resolves and the call at line 133 works. Add a guard that reports a clear message if the context value is missing, so a future regression does not surface asTypeError: selectWallet is not a functionbehind a generic toast.
📍 Affects 2 files
components/stellar/StellarProvider.jsx#L196-L201(this comment)hooks/useStellarAuth.js#L71-L72
🤖 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 `@components/stellar/StellarProvider.jsx` around lines 196 - 201, Define a
selectWallet callback in StellarProvider that opens the wallet picker and
returns the selected address, include it in the provider context value, and
retain it in the connect callback dependencies once defined. In
hooks/useStellarAuth.js, guard against a missing context selectWallet and report
a clear error before invoking it so regressions do not become a generic toast.
Affected sites: components/stellar/StellarProvider.jsx lines 196-201 require the
callback and context export; hooks/useStellarAuth.js lines 71-72 require the
missing-function guard.
| } catch (error) { | ||
| const message = error.response?.data?.message || error.message; | ||
|
|
||
| // Handle specific Stellar errors | ||
| if (message.includes("insufficient") || message.includes("underfunded")) { | ||
| toast.error("Insufficient USDC balance"); | ||
| } else if (message.includes("op_no_trust") || message.includes("trustline")) { | ||
| toast.error( | ||
| "You need to add USDC trustline to your wallet first" | ||
| ); | ||
| } else if (message.includes("rejected") || message.includes("cancelled")) { | ||
| toast.error("Transaction was cancelled"); | ||
| if (isUserRejection(error) || error.code === "USER_REJECTED") { | ||
| toast.info("Transaction cancelled", { | ||
| description: "You declined the signing request. No changes were made.", | ||
| }); | ||
| return false; | ||
| } | ||
|
|
||
| const mapped = mapStellarError(error); | ||
| if (mapped) { | ||
| toast.error(mapped.title, { | ||
| description: mapped.nextStep, | ||
| }); | ||
| } else { | ||
| const message = error.response?.data?.message || error.message; | ||
| toast.error(`Payment failed: ${message}`); | ||
| } | ||
| return false; | ||
| return { success: false, cancelled: isCancelled }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win
executePayment returns inconsistent shapes, and the consumer tests the result for truthiness. The producer returns { success: true, data }, { success: false }, and bare false from different paths. The consumer treats any truthy value as success, and every object is truthy. A failed payment therefore renders the success screen and fires onSuccess, which marks the item as purchased. Settle on one shape at the producer and read the flag at the consumer.
hooks/useStellarPayment.js#L90-L107: always return{ success: boolean, cancelled: boolean, data?, error? }. Replace the barefalseon line 95, and replace the undefinedisCancelledon line 107 withfalse.components/stellar/PaymentModal.jsx#L105-L114: changeif (success)to read the flag, for exampleconst result = await executePayment(paymentData); if (result?.success) { ... }. Handleresult.cancelledby returning to thepreviewstep instead of showing the error screen, since the user chose to cancel.
📍 Affects 2 files
hooks/useStellarPayment.js#L90-L107(this comment)components/stellar/PaymentModal.jsx#L105-L114
🤖 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 `@hooks/useStellarPayment.js` around lines 90 - 107, The payment result
contract is inconsistent, causing failed payment objects to be treated as
successful. In hooks/useStellarPayment.js lines 90-107, update executePayment to
always return { success, cancelled, data?, error? }, replacing the bare false
rejection result and undefined isCancelled with explicit boolean values. In
components/stellar/PaymentModal.jsx lines 105-114, inspect result.success rather
than result truthiness, and return to the preview step when result.cancelled is
true instead of showing the error screen.
Summary by CodeRabbit
New Features
Improvements