Skip to content

Commit a9e0ee8

Browse files
committed
Add progress bar for purchase intent sending
FEATURE: Real-time Progress Bar - Add progress tracking in PurchaseBusinessService with onProgress callback - Update usePurchaseIntent to track and expose progress (current/total/sellerPubkey) - Add visual progress bar in PurchaseConfirmationModal during sending - Progress bar shows: seller count (X of Y), percentage, animated bar UI Enhancements: - Blue-themed progress section between modal content and buttons - Smooth animated progress bar with transition effects - Real-time updates as each seller receives purchase intent - Clean percentage display and current status text Technical Implementation: - Modified sendPurchaseIntent() to accept optional onProgress callback - Progress state added to UsePurchaseIntentState interface - Real-time state updates via setState callback during send loop - Progress bar only shows when isLoading=true and progress data exists User Experience: - Users can now see exactly how many sellers are being contacted - Visual feedback shows progress instead of generic 'Sending...' - Builds confidence that the process is working - Transparent communication during multi-seller purchases
1 parent f2baa78 commit a9e0ee8

4 files changed

Lines changed: 81 additions & 11 deletions

File tree

src/components/shop/PurchaseConfirmationModal.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ interface PurchaseConfirmationModalProps {
1313
items: CartItem[];
1414
total: number;
1515
isLoading?: boolean;
16+
progress?: {
17+
current: number;
18+
total: number;
19+
sellerPubkey: string;
20+
} | null;
1621
}
1722

1823
interface SellerGroup {
@@ -38,6 +43,7 @@ export default function PurchaseConfirmationModal({
3843
items,
3944
total,
4045
isLoading = false,
46+
progress = null,
4147
}: PurchaseConfirmationModalProps) {
4248
const [sellerProfiles, setSellerProfiles] = useState<Map<string, SellerProfile>>(new Map());
4349

@@ -246,6 +252,31 @@ export default function PurchaseConfirmationModal({
246252
</div>
247253
</div>
248254

255+
{/* Progress Bar - shown during loading */}
256+
{isLoading && progress && (
257+
<div className="px-6 py-4 border-t border-gray-200 bg-blue-50">
258+
<div className="space-y-2">
259+
<div className="flex justify-between items-center text-sm">
260+
<span className="font-medium text-blue-900">
261+
Sending to sellers ({progress.current} of {progress.total})
262+
</span>
263+
<span className="text-blue-700">
264+
{Math.round((progress.current / progress.total) * 100)}%
265+
</span>
266+
</div>
267+
<div className="w-full bg-blue-200 rounded-full h-2.5">
268+
<div
269+
className="bg-blue-600 h-2.5 rounded-full transition-all duration-300 ease-out"
270+
style={{ width: `${(progress.current / progress.total) * 100}%` }}
271+
/>
272+
</div>
273+
<p className="text-xs text-blue-700">
274+
Processing seller {progress.current}...
275+
</p>
276+
</div>
277+
</div>
278+
)}
279+
249280
{/* Footer - Buttons */}
250281
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50">
251282
<div className="flex gap-3 justify-end">

src/components/shop/PurchaseIntentButton.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export function PurchaseIntentButton({ disabled = false, className = '' }: Purch
1818
const router = useRouter();
1919
const [showModal, setShowModal] = useState(false);
2020
const { items } = useCartStore();
21-
const { loading, success, error, result, sendPurchaseIntent, reset } = usePurchaseIntent();
21+
const { loading, success, error, result, progress, sendPurchaseIntent, reset } = usePurchaseIntent();
2222

2323
// Calculate total in sats
2424
const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
@@ -137,6 +137,7 @@ export function PurchaseIntentButton({ disabled = false, className = '' }: Purch
137137
items={items}
138138
total={total}
139139
isLoading={loading}
140+
progress={progress}
140141
/>
141142
</>
142143
);

src/hooks/usePurchaseIntent.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ interface UsePurchaseIntentState {
2222
success: boolean;
2323
error: string | null;
2424
result: PurchaseIntentResult | null;
25+
progress: {
26+
current: number;
27+
total: number;
28+
sellerPubkey: string;
29+
} | null;
2530
}
2631

2732
interface UsePurchaseIntentReturn extends UsePurchaseIntentState {
@@ -35,6 +40,7 @@ export function usePurchaseIntent(): UsePurchaseIntentReturn {
3540
success: false,
3641
error: null,
3742
result: null,
43+
progress: null,
3844
});
3945

4046
const signer = useAuthStore(state => state.signer);
@@ -58,6 +64,7 @@ export function usePurchaseIntent(): UsePurchaseIntentReturn {
5864
success: false,
5965
error: null,
6066
result: null,
67+
progress: null,
6168
});
6269

6370
try {
@@ -89,8 +96,18 @@ export function usePurchaseIntent(): UsePurchaseIntentReturn {
8996
itemCount: items.length,
9097
});
9198

92-
// Send purchase intent via business service
93-
const result = await purchaseBusinessService.sendPurchaseIntent(items, signer);
99+
// Send purchase intent via business service with progress callback
100+
const result = await purchaseBusinessService.sendPurchaseIntent(
101+
items,
102+
signer,
103+
(current, total, sellerPubkey) => {
104+
// Update progress in real-time
105+
setState(prev => ({
106+
...prev,
107+
progress: { current, total, sellerPubkey }
108+
}));
109+
}
110+
);
94111

95112
if (result.success) {
96113
// Success - set state first, then clear cart
@@ -99,6 +116,7 @@ export function usePurchaseIntent(): UsePurchaseIntentReturn {
99116
success: true,
100117
error: null,
101118
result,
119+
progress: null,
102120
});
103121

104122
logger.info('Purchase intent sent successfully - will clear cart after redirect', {
@@ -129,6 +147,7 @@ export function usePurchaseIntent(): UsePurchaseIntentReturn {
129147
success: false,
130148
error: result.error || 'Failed to send purchase intent',
131149
result,
150+
progress: null,
132151
});
133152

134153
logger.warn('Purchase intent partially failed', {
@@ -151,6 +170,7 @@ export function usePurchaseIntent(): UsePurchaseIntentReturn {
151170
success: false,
152171
error: errorMessage,
153172
result: null,
173+
progress: null,
154174
});
155175

156176
logger.error('Purchase intent failed', error instanceof Error ? error : new Error('Unknown error'), {
@@ -170,6 +190,7 @@ export function usePurchaseIntent(): UsePurchaseIntentReturn {
170190
success: false,
171191
error: null,
172192
result: null,
193+
progress: null,
173194
});
174195
}, []);
175196

src/services/business/PurchaseBusinessService.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,23 +183,23 @@ export class PurchaseBusinessService {
183183
}
184184

185185
/**
186-
* Send purchase intent to all sellers
186+
* Send purchase intent to all sellers via encrypted messaging (NIP-17)
187187
*
188-
* @param items - Cart items
189-
* @param signer - Nostr signer for message encryption
188+
* @param items - Cart items to send
189+
* @param signer - Nostr signer for encryption and signing
190+
* @param onProgress - Optional callback for progress updates
190191
* @returns Result of purchase intent sends
191192
*/
192193
public async sendPurchaseIntent(
193194
items: CartItem[],
194-
signer: NostrSigner
195+
signer: NostrSigner,
196+
onProgress?: (current: number, total: number, sellerPubkey: string) => void
195197
): Promise<PurchaseIntentResult> {
196198
logger.info('Sending purchase intent', {
197199
service: 'PurchaseBusinessService',
198200
method: 'sendPurchaseIntent',
199201
itemCount: items.length,
200-
});
201-
202-
// Validate cart
202+
}); // Validate cart
203203
const validation = this.validateCart(items);
204204
if (!validation.valid) {
205205
logger.error('Cart validation failed', new Error('Cart validation failed'), {
@@ -225,7 +225,9 @@ export class PurchaseBusinessService {
225225
let successCount = 0;
226226
let failureCount = 0;
227227

228-
for (const sellerGroup of sellerGroups) {
228+
for (let i = 0; i < sellerGroups.length; i++) {
229+
const sellerGroup = sellerGroups[i];
230+
229231
try {
230232
// Prepare message content
231233
const messageContent = this.preparePurchaseIntent(sellerGroup);
@@ -250,6 +252,11 @@ export class PurchaseBusinessService {
250252
sellerPubkey: sellerGroup.sellerPubkey,
251253
messageId: sendResult.message?.id,
252254
});
255+
256+
// Report progress
257+
if (onProgress) {
258+
onProgress(i + 1, sellerGroups.length, sellerGroup.sellerPubkey);
259+
}
253260
} else {
254261
failureCount++;
255262
results.push({
@@ -263,6 +270,11 @@ export class PurchaseBusinessService {
263270
method: 'sendPurchaseIntent',
264271
sellerPubkey: sellerGroup.sellerPubkey,
265272
});
273+
274+
// Report progress even on failure
275+
if (onProgress) {
276+
onProgress(i + 1, sellerGroups.length, sellerGroup.sellerPubkey);
277+
}
266278
}
267279
} catch (error) {
268280
failureCount++;
@@ -278,6 +290,11 @@ export class PurchaseBusinessService {
278290
method: 'sendPurchaseIntent',
279291
sellerPubkey: sellerGroup.sellerPubkey,
280292
});
293+
294+
// Report progress even on exception
295+
if (onProgress) {
296+
onProgress(i + 1, sellerGroups.length, sellerGroup.sellerPubkey);
297+
}
281298
}
282299
}
283300

0 commit comments

Comments
 (0)