feat(purchases): add My Purchases library with Stellar payment receipts - #140
Conversation
- Create `usePurchases` hook to aggregate owned courses and books with corresponding Stellar payment transactions
- Correlate purchased content using buyer-role transaction history while supporting pagination-safe aggregation
- Handle free enrollments gracefully by distinguishing unpaid content from paid purchases
- Expose reusable receipt lookup logic for retrieving confirmed transaction details per purchased item
- Add `/dashboard/purchases` page featuring:
- Separate Courses and Books tabs with owned item counts
- Responsive purchase cards with thumbnails, category badges, ownership status, pricing, and direct Watch/Read actions
- Purchase receipt modal displaying:
- USDC payment amount
- Transaction status
- Purchase date
- Creator information
- Truncated wallet addresses
- Stellar Explorer transaction link
- Printable browser-friendly receipt view
- "Free Enrollment" state for items without an associated on-chain transaction
- Loading skeletons, empty state with browse CTAs, and error state with retry support
- Add "My Purchases" navigation item to the dashboard sidebar with ShoppingBag icon
Implementation notes:
- Reuses existing purchase ownership data from `usePurchase`
- Integrates with Stellar transaction history instead of duplicating wallet transaction logic
- Reuses existing status color mappings and wallet address formatting utilities for UI consistency
- Supports all buyer transaction pages to ensure complete purchase history correlation
- Preserves existing dashboard architecture while introducing a dedicated content-centric purchase experience
Verification:
- Verified owned courses and books render correctly with appropriate Watch/Read actions
- Confirmed paid purchases display complete Stellar receipt details with working explorer links
- Validated free enrollments display a "Free Enrollment" state without broken receipts
- Confirmed loading, empty, retry, and error states function correctly
- `npm run lint` passes (pre-existing warnings only)
- `npm run build` introduces no new build issues (existing Firebase/Firestore issues are unrelated)
|
@IamOluwatoyin is attempting to deploy a commit to the Deen Bridge Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
WalkthroughAdds a ChangesMy Purchases dashboard
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant PurchasesPage
participant usePurchases
participant CourseBookAPIs
participant StellarTransactions
User->>PurchasesPage: open My Purchases
PurchasesPage->>usePurchases: load owned content
usePurchases->>CourseBookAPIs: fetch courses and books
usePurchases->>StellarTransactions: fetch buyer transactions
CourseBookAPIs-->>usePurchases: content lists
StellarTransactions-->>usePurchases: confirmed transaction history
usePurchases-->>PurchasesPage: owned items and receipts
PurchasesPage-->>User: render cards and purchase details
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
app/dashboard/purchases/page.jsx (2)
300-302: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win"Try Again" reloads the whole page instead of retrying the fetch in place.
usePurchasesdoesn't expose a refetch function, so this falls back towindow.location.reload(), which discards all other client-side state (tab selection, scroll position, etc.) for what should be a scoped retry.Consider exposing a
refetchfromusePurchases(e.g. extractingloadfrom the effect) and calling that here instead of forcing a full reload.🤖 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/purchases/page.jsx` around lines 300 - 302, Update usePurchases to expose a refetch function by extracting its fetch/load logic from the effect, then wire the “Try Again” Button handler to call refetch instead of window.location.reload(). Preserve the existing purchase-loading behavior and client-side state.
166-171: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winAdd a
sizesprop tofill-modenext/image.Without
sizes, Next.js can't optimize the responsive srcset for this hero thumbnail, and browsers may request a larger image than the rendered card actually needs.<Image src={thumbnail} alt={title} fill + sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw" className="object-cover transition-transform duration-500 group-hover:scale-105" />🤖 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/purchases/page.jsx` around lines 166 - 171, Add a responsive sizes prop to the fill-mode Image component rendering the purchase thumbnail, using values that match the card’s rendered widths across breakpoints. Keep the existing src, alt, fill, and styling unchanged.
🤖 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 `@app/dashboard/purchases/page.jsx`:
- Around line 44-147: Implement the printable receipt requirement in
ReceiptModal by adding a clearly labeled Print Receipt action that invokes
window.print() and applying print-specific styling so only the receipt content
is presented when printing. Keep the existing on-screen modal layout and
receipt/free-enrollment content unchanged.
- Around line 149-190: Update PurchaseCard to use the hook’s exported isFreeItem
classification when rendering the payment-status Badge, distinguishing genuinely
free items from paid items whose receipt lookup returns no match. Preserve the
Paid state for found receipts, show Enrolled only for confirmed free items, and
avoid labeling unmatched paid purchases as free.
In `@hooks/usePurchases.js`:
- Around line 46-65: Update fetchAllBuyerTransactions to handle unsuccessful
getTransactionHistory results instead of silently advancing to the next page:
propagate the failure to the caller, or apply the hook’s existing retry/error
mechanism, and ensure getReceipt cannot treat incomplete transaction data as an
empty successful history. Preserve accumulation and pagination behavior for
successful pages.
- Around line 124-134: Update the receipt mapping in usePurchases to include the
transaction’s network value in the returned object alongside the existing
receipt fields, enabling the purchases dashboard to render Stellar network
details.
---
Nitpick comments:
In `@app/dashboard/purchases/page.jsx`:
- Around line 300-302: Update usePurchases to expose a refetch function by
extracting its fetch/load logic from the effect, then wire the “Try Again”
Button handler to call refetch instead of window.location.reload(). Preserve the
existing purchase-loading behavior and client-side state.
- Around line 166-171: Add a responsive sizes prop to the fill-mode Image
component rendering the purchase thumbnail, using values that match the card’s
rendered widths across breakpoints. Keep the existing src, alt, fill, and
styling unchanged.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: fb2369ac-7e9c-4759-bf21-1d18a1d035c2
📒 Files selected for processing (3)
app/dashboard/purchases/page.jsxcomponents/molecules/dashboard/nav-routers.jsxhooks/usePurchases.js
| function ReceiptModal({ open, onClose, item, receipt, itemType }) { | ||
| if (!open) return null; | ||
|
|
||
| return ( | ||
| <Dialog open={open} onOpenChange={onClose}> | ||
| <DialogContent className="sm:max-w-md"> | ||
| <DialogHeader> | ||
| <DialogTitle className="flex items-center gap-2"> | ||
| <Receipt className="h-5 w-5" /> | ||
| Payment Receipt | ||
| </DialogTitle> | ||
| <DialogDescription> | ||
| Transaction details for your purchase | ||
| </DialogDescription> | ||
| </DialogHeader> | ||
|
|
||
| <div className="space-y-4"> | ||
| <div className="p-4 bg-muted rounded-lg space-y-2"> | ||
| <div className="flex items-center gap-2"> | ||
| {itemType === "book" ? ( | ||
| <BookOpen className="h-4 w-4 text-muted-foreground" /> | ||
| ) : ( | ||
| <GraduationCap className="h-4 w-4 text-muted-foreground" /> | ||
| )} | ||
| <h4 className="font-semibold">{item?.title}</h4> | ||
| </div> | ||
| <Badge variant="outline" className="capitalize"> | ||
| {itemType} | ||
| </Badge> | ||
| </div> | ||
|
|
||
| {receipt ? ( | ||
| <div className="space-y-3"> | ||
| <div className="flex justify-between items-center p-3 bg-green-50 border border-green-200 rounded-lg"> | ||
| <span className="text-sm text-green-700 font-medium">Amount Paid</span> | ||
| <span className="text-xl font-bold text-green-700"> | ||
| ${receipt.amount?.toFixed(2)} USDC | ||
| </span> | ||
| </div> | ||
|
|
||
| <div className="grid grid-cols-2 gap-3"> | ||
| <div className="p-3 border rounded-lg"> | ||
| <p className="text-xs text-muted-foreground">Status</p> | ||
| <Badge variant="secondary" className={`mt-1 ${statusColors[receipt.status]}`}> | ||
| {receipt.status} | ||
| </Badge> | ||
| </div> | ||
| <div className="p-3 border rounded-lg"> | ||
| <p className="text-xs text-muted-foreground">Date</p> | ||
| <p className="text-sm font-medium mt-1"> | ||
| {dayjs(receipt.createdAt).format("MMM D, YYYY")} | ||
| </p> | ||
| </div> | ||
| </div> | ||
|
|
||
| <div className="p-3 border rounded-lg"> | ||
| <p className="text-xs text-muted-foreground">Paid To (Creator)</p> | ||
| <p className="text-sm font-medium mt-1"> | ||
| {receipt.creatorName || "Creator"} | ||
| </p> | ||
| {receipt.creatorWallet && ( | ||
| <p className="text-xs font-mono text-muted-foreground mt-0.5"> | ||
| {truncateAddress(receipt.creatorWallet)} | ||
| </p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div className="p-3 border rounded-lg"> | ||
| <p className="text-xs text-muted-foreground">Your Wallet</p> | ||
| {receipt.buyerWallet && ( | ||
| <p className="text-sm font-mono mt-1"> | ||
| {truncateAddress(receipt.buyerWallet)} | ||
| </p> | ||
| )} | ||
| </div> | ||
|
|
||
| {receipt.explorerUrl && ( | ||
| <a | ||
| href={receipt.explorerUrl} | ||
| target="_blank" | ||
| rel="noopener noreferrer" | ||
| className="inline-flex items-center gap-1 text-sm text-primary hover:underline" | ||
| > | ||
| <ExternalLink className="h-4 w-4" /> | ||
| View on Stellar Explorer | ||
| </a> | ||
| )} | ||
| </div> | ||
| ) : ( | ||
| <div className="p-4 border border-blue-200 bg-blue-50 rounded-lg text-center"> | ||
| <p className="text-sm text-blue-700 font-medium"> | ||
| Free Enrollment | ||
| </p> | ||
| <p className="text-xs text-blue-600 mt-1"> | ||
| This item was free or enrolled without a Stellar payment. No on-chain | ||
| transaction receipt is available. | ||
| </p> | ||
| </div> | ||
| )} | ||
| </div> | ||
| </DialogContent> | ||
| </Dialog> | ||
| ); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
No print action implemented for "printable per-purchase receipts."
The PR objectives call out printable receipts as a requirement, but ReceiptModal has no print button, window.print() call, or print-specific styling. Please confirm whether this was intentionally deferred to a follow-up, or should be added here.
Happy to add a "Print Receipt" button with a print-friendly layout if useful — want me to draft it?
🤖 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/purchases/page.jsx` around lines 44 - 147, Implement the
printable receipt requirement in ReceiptModal by adding a clearly labeled Print
Receipt action that invokes window.print() and applying print-specific styling
so only the receipt content is presented when printing. Keep the existing
on-screen modal layout and receipt/free-enrollment content unchanged.
| function PurchaseCard({ item, itemType, getReceipt }) { | ||
| const [receiptOpen, setReceiptOpen] = useState(false); | ||
| const receipt = getReceipt(item._id, itemType, item.title); | ||
| const thumbnail = item.thumbnail || item.image || "/images/dnb.png"; | ||
| const title = item.title || "Untitled"; | ||
| const authorName = item.createdBy?.name || item.author?.name || "Unknown"; | ||
| const authorAvatar = item.createdBy?.avatar || item.author?.avatar || "/images/placeholder.jpg"; | ||
| const actionHref = | ||
| itemType === "course" | ||
| ? `/dashboard/courses/${item._id}` | ||
| : `/dashboard/library/read/${item._id}`; | ||
| const actionLabel = itemType === "course" ? "Watch Course" : "Read Book"; | ||
|
|
||
| return ( | ||
| <> | ||
| <Card className="relative flex flex-col overflow-hidden rounded-2xl bg-muted/30 backdrop-blur-xl shadow-lg hover:shadow-2xl hover:scale-[1.015] transition-all group"> | ||
| <div className="relative h-48 w-full"> | ||
| <Image | ||
| src={thumbnail} | ||
| alt={title} | ||
| fill | ||
| className="object-cover transition-transform duration-500 group-hover:scale-105" | ||
| /> | ||
| <div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent" /> | ||
| <div className="absolute top-3 left-3"> | ||
| <Badge className="bg-white/80 text-accent font-bold px-3 py-1 rounded-full shadow border-0 text-xs uppercase tracking-wider"> | ||
| {item.category || itemType} | ||
| </Badge> | ||
| </div> | ||
| <div className="absolute bottom-3 left-3 right-3 flex items-center justify-between"> | ||
| <Badge variant="secondary" className="bg-black/60 text-white border-0"> | ||
| {item.price > 0 ? `$${item.price} USDC` : "Free"} | ||
| </Badge> | ||
| {receipt ? ( | ||
| <Badge variant="secondary" className="bg-green-600/80 text-white border-0"> | ||
| Paid | ||
| </Badge> | ||
| ) : ( | ||
| <Badge variant="secondary" className="bg-blue-600/80 text-white border-0"> | ||
| Enrolled | ||
| </Badge> | ||
| )} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Badge only reflects "receipt found or not", never distinguishes true free items from unmatched paid ones.
PurchaseCard renders "Enrolled" whenever getReceipt returns nothing — regardless of whether item.price is 0 (actually free) or greater than 0 but the transaction lookup simply failed to match. The hook already exports isFreeItem for exactly this distinction, but it's never imported/used here, so a paid item whose receipt correlation fails (see the title-matching concern in hooks/usePurchases.js) is silently mislabeled as a free enrollment.
🤖 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/purchases/page.jsx` around lines 149 - 190, Update PurchaseCard
to use the hook’s exported isFreeItem classification when rendering the
payment-status Badge, distinguishing genuinely free items from paid items whose
receipt lookup returns no match. Preserve the Paid state for found receipts,
show Enrolled only for confirmed free items, and avoid labeling unmatched paid
purchases as free.
| const fetchAllBuyerTransactions = useCallback(async () => { | ||
| let all = []; | ||
| let page = 1; | ||
| let totalPages = 1; | ||
|
|
||
| while (page <= totalPages) { | ||
| const result = await getTransactionHistory({ | ||
| role: "buyer", | ||
| page, | ||
| limit: 100, | ||
| }); | ||
| if (result.success) { | ||
| all = [...all, ...result.transactions]; | ||
| totalPages = result.pagination?.pages || 1; | ||
| } | ||
| page++; | ||
| } | ||
|
|
||
| return all; | ||
| }, [getTransactionHistory]); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Failed transaction pages are silently dropped instead of surfaced/retried.
When getTransactionHistory returns result.success === false for a page in the middle of pagination, that page's transactions are skipped, totalPages isn't updated, and the loop just moves to page++ — the caller never learns the transaction history is incomplete. Since getReceipt relies on this data to correlate purchases with Stellar transactions, a transient failure on any page silently turns paid items into "Free/Enrolled" in the UI, with no error state or retry.
🛠️ Proposed fix to propagate partial failures
const fetchAllBuyerTransactions = useCallback(async () => {
let all = [];
let page = 1;
let totalPages = 1;
while (page <= totalPages) {
const result = await getTransactionHistory({
role: "buyer",
page,
limit: 100,
});
- if (result.success) {
- all = [...all, ...result.transactions];
- totalPages = result.pagination?.pages || 1;
- }
+ if (!result.success) {
+ throw new Error("Failed to load full transaction history");
+ }
+ all = [...all, ...result.transactions];
+ totalPages = result.pagination?.pages || 1;
page++;
}
return all;
}, [getTransactionHistory]);📝 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.
| const fetchAllBuyerTransactions = useCallback(async () => { | |
| let all = []; | |
| let page = 1; | |
| let totalPages = 1; | |
| while (page <= totalPages) { | |
| const result = await getTransactionHistory({ | |
| role: "buyer", | |
| page, | |
| limit: 100, | |
| }); | |
| if (result.success) { | |
| all = [...all, ...result.transactions]; | |
| totalPages = result.pagination?.pages || 1; | |
| } | |
| page++; | |
| } | |
| return all; | |
| }, [getTransactionHistory]); | |
| const fetchAllBuyerTransactions = useCallback(async () => { | |
| let all = []; | |
| let page = 1; | |
| let totalPages = 1; | |
| while (page <= totalPages) { | |
| const result = await getTransactionHistory({ | |
| role: "buyer", | |
| page, | |
| limit: 100, | |
| }); | |
| if (!result.success) { | |
| throw new Error("Failed to load full transaction history"); | |
| } | |
| all = [...all, ...result.transactions]; | |
| totalPages = result.pagination?.pages || 1; | |
| page++; | |
| } | |
| return all; | |
| }, [getTransactionHistory]); |
🤖 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/usePurchases.js` around lines 46 - 65, Update fetchAllBuyerTransactions
to handle unsuccessful getTransactionHistory results instead of silently
advancing to the next page: propagate the failure to the caller, or apply the
hook’s existing retry/error mechanism, and ensure getReceipt cannot treat
incomplete transaction data as an empty successful history. Preserve
accumulation and pagination behavior for successful pages.
| return { | ||
| amount: tx.amount, | ||
| status: tx.status, | ||
| createdAt: tx.createdAt, | ||
| buyerWallet: tx.buyerWallet, | ||
| creatorWallet: tx.creatorWallet, | ||
| creatorName: tx.creator?.name, | ||
| buyerName: tx.buyer?.name, | ||
| explorerUrl: tx.explorerUrl, | ||
| _id: tx._id, | ||
| }; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== File list ==\n'
git ls-files 'hooks/usePurchases.js' 'page.jsx' '**/page.jsx' '**/*purchase*' '**/*Purchase*' | sed 's#^\./##'
printf '\n== Locate network usage around purchases ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' '\bnetwork\b|explorerUrl|buyerWallet|creatorWallet|buyerName|creatorName' hooks page.jsx . || true
printf '\n== Read hooks/usePurchases.js (around cited lines) ==\n'
if [ -f hooks/usePurchases.js ]; then
nl -ba hooks/usePurchases.js | sed -n '1,220p'
fi
printf '\n== Read page.jsx if present ==\n'
if [ -f page.jsx ]; then
nl -ba page.jsx | sed -n '1,240p'
fiRepository: Deen-Bridge/dnb-frontend
Length of output: 38133
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== hooks/usePurchases.js ==\n'
sed -n '1,220p' hooks/usePurchases.js
printf '\n== app/dashboard/purchases/page.jsx ==\n'
sed -n '1,260p' app/dashboard/purchases/page.jsxRepository: Deen-Bridge/dnb-frontend
Length of output: 14396
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== hooks/useStellarPayment.js ==\n'
sed -n '1,240p' hooks/useStellarPayment.js
printf '\n== components/stellar/TransactionHistory.jsx ==\n'
sed -n '1,260p' components/stellar/TransactionHistory.jsx
printf '\n== Search for transaction network fields ==\n'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' '\btx\.network\b|\bnetwork:\s*|getTransactionHistory\(|transactionHistory|explorerUrl' hooks components app lib . || trueRepository: Deen-Bridge/dnb-frontend
Length of output: 16189
Include network in the receipt payload
The receipt returned by hooks/usePurchases.js still omits network, so app/dashboard/purchases/page.jsx can’t render the Stellar network badge/details the PR is supposed to show. Add it to the mapped object alongside the other receipt fields.
🤖 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/usePurchases.js` around lines 124 - 134, Update the receipt mapping in
usePurchases to include the transaction’s network value in the returned object
alongside the existing receipt fields, enabling the purchases dashboard to
render Stellar network details.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@components/molecules/dashboard/nav-routers.jsx`:
- Around line 13-14: Add the missing comma after ShoppingBag in the import list,
keeping DollarSign as the following imported symbol so the file parses
correctly.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: bc0c61fe-1b95-4ff8-ac9e-f6b001bd3d0a
📒 Files selected for processing (1)
components/molecules/dashboard/nav-routers.jsx
|
@IamOluwatoyin please fix CI |
Create
usePurchaseshook to aggregate owned courses and books with corresponding Stellar payment transactionsCorrelate purchased content using buyer-role transaction history while supporting pagination-safe aggregation
Handle free enrollments gracefully by distinguishing unpaid content from paid purchases
Expose reusable receipt lookup logic for retrieving confirmed transaction details per purchased item
Add
/dashboard/purchasespage featuring:Add "My Purchases" navigation item to the dashboard sidebar with ShoppingBag icon
Implementation notes:
usePurchaseVerification:
npm run lintpasses (pre-existing warnings only)npm run buildintroduces no new build issues (existing Firebase/Firestore issues are unrelated)Close #130
Summary by CodeRabbit