Skip to content

Commit 9c4149d

Browse files
committed
Implement hybrid profile fetching with NIP-42 authentication
- Add environment-based NDK configuration with relay URLs - Implement NIP-42 authentication using NDKRelayAuthPolicies.signIn - Create hybrid profile fetching: NDK first, backend API fallback - Fix authentication issues in LoginForm and useWalletAuth - Improve display name parsing from Nostr profile data - Prevent infinite loops with better profile caching logic - Remove global Window.nostr declaration to avoid NDK conflicts
1 parent 3bd9695 commit 9c4149d

10 files changed

Lines changed: 131 additions & 53 deletions

File tree

.env.development

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ REACT_APP_ASSETS_BUCKET=http://localhost
44
REACT_APP_DEMO_MODE=false
55
REACT_APP_BASENAME=
66

7+
# Nostr relay configuration for profile fetching
8+
REACT_APP_OWN_RELAY_URL=ws://localhost:7000
9+
REACT_APP_NOSTR_RELAY_URLS=wss://relay.damus.io,wss://relay.nostr.band,wss://relay.snort.social,wss://vault.iris.to
10+
711
# More info https://create-react-app.dev/docs/advanced-configuration
812
ESLINT_NO_DEV_ERRORS=true
913
TSC_COMPILE_ON_ERROR=true

src/App.tsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,33 @@ import { usePWA } from './hooks/usePWA';
1313
import { useThemeWatcher } from './hooks/useThemeWatcher';
1414
import { useAppSelector } from './hooks/reduxHooks';
1515
import { themeObject } from './styles/themes/themeVariables';
16-
import NDK from '@nostr-dev-kit/ndk';
16+
import NDK, { NDKEvent, NDKNip07Signer, NDKRelayAuthPolicies } from '@nostr-dev-kit/ndk';
1717
import { useNDKInit } from '@nostr-dev-kit/ndk-hooks';
18+
import config from './config/config';
19+
20+
// Configure NDK with user's relay URLs from environment variables
21+
const getRelayUrls = () => {
22+
const relayUrls = [...config.nostrRelayUrls];
23+
24+
// Add user's own relay URL as the first priority if provided
25+
if (config.ownRelayUrl) {
26+
relayUrls.unshift(config.ownRelayUrl);
27+
}
28+
29+
return relayUrls;
30+
};
1831

1932
const ndk = new NDK({
20-
explicitRelayUrls: ['wss://relay.damus.io', 'wss://relay.nostr.band', 'wss://relay.snort.social', 'vault.iris.to'],
33+
explicitRelayUrls: getRelayUrls(),
34+
signer: new NDKNip07Signer(),
2135
});
36+
37+
// Set up NIP-42 authentication policy following the example
38+
ndk.relayAuthDefaultPolicy = NDKRelayAuthPolicies.signIn({ ndk });
39+
2240
ndk
2341
.connect()
24-
.then(() => console.log('NDK connected'))
42+
.then(() => console.log('NDK connected with relay URLs and NIP-42 auth policy:', getRelayUrls()))
2543
.catch((error) => console.error('NDK connection error:', error));
2644

2745
const App: React.FC = () => {

src/components/auth/LoginForm/LoginForm.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ export const LoginForm: React.FC = () => {
7070
console.log('Signed event:', signedEvent);
7171

7272
const response = await verifyChallenge({
73-
challenge: signedEvent.content,
73+
challenge: event.content,
7474
signature: signedEvent.sig,
75-
messageHash: signedEvent.id,
75+
messageHash: event.id,
7676
event: signedEvent,
7777
});
7878

src/components/relay-dashboard/paid-subscribers/PaidSubscribers.tsx

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -93,48 +93,94 @@ export const PaidSubscribers: React.FC = () => {
9393
};
9494

9595
useEffect(() => {
96-
// Fetch profiles for test subscribers
96+
// Implement hybrid profile fetching: NDK first, fallback to backend data
9797
if (useDummyData) {
9898
console.warn('[PaidSubscribers] Using dummy data, skipping profile fetch');
9999
setLoadingProfiles(false);
100100
return;
101101
}
102+
102103
const fetchProfiles = async () => {
103104
if (!ndkInstance || !ndkInstance.ndk) {
104-
console.error('NDK instance is not initialized');
105+
console.error('[PaidSubscribers] NDK instance is not initialized, using backend data only');
106+
setLoadingProfiles(false);
105107
return;
106108
}
107-
//1. map through subscribers and fetch profiles. skip profile if already on map
109+
110+
console.log('[PaidSubscribers] Starting hybrid profile fetch for', subscribers.length, 'subscribers');
111+
112+
// Process each subscriber with hybrid approach
108113
await Promise.all(
109114
subscribers.map(async (subscriber) => {
110-
if (
111-
subscriberProfiles.has(subscriber.pubkey) &&
112-
subscriberProfiles.get(subscriber.pubkey)?.picture &&
113-
subscriberProfiles.get(subscriber.pubkey)?.about
114-
) {
115-
return subscriberProfiles.get(subscriber.pubkey);
115+
// Skip if we already have a complete profile in our map
116+
const existingProfile = subscriberProfiles.get(subscriber.pubkey);
117+
const hasValidProfile = existingProfile && (
118+
(existingProfile.name && existingProfile.name !== 'Anonymous Subscriber') ||
119+
existingProfile.picture ||
120+
existingProfile.about
121+
);
122+
123+
if (hasValidProfile) {
124+
console.log(`[PaidSubscribers] Profile already cached for ${subscriber.pubkey}:`, {
125+
name: existingProfile.name,
126+
picture: !!existingProfile.picture,
127+
about: !!existingProfile.about
128+
});
129+
return;
116130
}
131+
117132
try {
118-
if (!ndkInstance.ndk) {
119-
console.error('NDK instance is not available');
120-
return null;
121-
}
133+
console.log(`[PaidSubscribers] Fetching NDK profile for ${subscriber.pubkey}`);
134+
135+
// Try to fetch profile from NDK (user's relay + other relays)
122136
const user = await ndkInstance.ndk?.getUser({ pubkey: subscriber.pubkey }).fetchProfile();
123-
if (user) {
124-
// Convert NDKUserProfile to SubscriberProfile and add to map
125-
const covertedNDKUserProfile = convertNDKUserProfileToSubscriberProfile(subscriber.pubkey, user);
126-
updateSubscriberProfile(subscriber.pubkey, covertedNDKUserProfile);
127-
128-
return user;
137+
138+
if (user && (user.name || user.picture || user.about)) {
139+
// NDK returned a profile - use it as the primary source
140+
console.log(`[PaidSubscribers] NDK profile found for ${subscriber.pubkey}:`, {
141+
name: user.name,
142+
picture: user.picture,
143+
about: user.about
144+
});
145+
146+
const ndkProfile = convertNDKUserProfileToSubscriberProfile(subscriber.pubkey, user);
147+
updateSubscriberProfile(subscriber.pubkey, ndkProfile);
148+
} else {
149+
// NDK came up empty - fallback to backend data
150+
console.log(`[PaidSubscribers] NDK profile empty for ${subscriber.pubkey}, falling back to backend data:`, {
151+
name: subscriber.name,
152+
picture: subscriber.picture,
153+
about: subscriber.about
154+
});
155+
156+
// Use the backend data as-is since NDK had no better information
157+
updateSubscriberProfile(subscriber.pubkey, {
158+
...subscriber,
159+
// Ensure we have fallback values if backend data is also incomplete
160+
name: subscriber.name || 'Anonymous Subscriber',
161+
picture: subscriber.picture || '',
162+
about: subscriber.about || ''
163+
});
129164
}
130165
} catch (error) {
131-
console.error(`Error fetching profile for ${subscriber.pubkey}:`, error);
166+
console.error(`[PaidSubscribers] Error fetching NDK profile for ${subscriber.pubkey}:`, error);
167+
168+
// Error occurred - fallback to backend data
169+
console.log(`[PaidSubscribers] NDK error for ${subscriber.pubkey}, using backend data`);
170+
updateSubscriberProfile(subscriber.pubkey, {
171+
...subscriber,
172+
name: subscriber.name || 'Anonymous Subscriber',
173+
picture: subscriber.picture || '',
174+
about: subscriber.about || ''
175+
});
132176
}
133-
return null;
134177
}),
135178
);
179+
180+
console.log('[PaidSubscribers] Hybrid profile fetch completed');
136181
setLoadingProfiles(false);
137182
};
183+
138184
fetchProfiles();
139185
}, [subscribers, ndkInstance]);
140186

src/components/relay-dashboard/paid-subscribers/SubscriberDetailModal/SubscriberDetailModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const SubscriberDetailModal: React.FC<SubscriberDetailModalProps> = ({ su
4343
if (!subscriber && !loading && !fetchFailed) {
4444
return (
4545
<S.StateModal open={isVisible} footer={null} onCancel={onClose} centered>
46-
<Typography.Text type="secondary">Couldn't find this subscriber profile.</Typography.Text>
46+
<Typography.Text type="secondary">Couldn&apos;t find this subscriber profile.</Typography.Text>
4747
</S.StateModal>
4848
);
4949
}

src/config/config.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@ const config = {
88
isDemoMode: process.env.REACT_APP_DEMO_MODE === 'true',
99
walletBaseURL: process.env.REACT_APP_WALLET_BASE_URL?.trim() || 'http://localhost:9003',
1010

11+
// Nostr relay configuration
12+
nostrRelayUrls: process.env.REACT_APP_NOSTR_RELAY_URLS?.split(',').map(url => url.trim()) || [
13+
'wss://relay.damus.io',
14+
'wss://relay.nostr.band',
15+
'wss://relay.snort.social',
16+
'wss://vault.iris.to'
17+
],
18+
19+
// User's own relay URL (primary relay for profile fetching)
20+
ownRelayUrl: process.env.REACT_APP_OWN_RELAY_URL?.trim() || null,
21+
1122
// Notification settings
1223
notifications: {
1324
// 5 minutes in milliseconds

src/hooks/usePaidSubscribers.ts

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -146,15 +146,14 @@ const usePaidSubscribers = (pageSize = 20) => {
146146
console.log(`[usePaidSubscribers] Normalized data:`, data);
147147
console.log(`[usePaidSubscribers] Data length: ${data?.length}, typeof data: ${typeof data}, Array.isArray(data): ${Array.isArray(data)}`);
148148

149-
// *** NEW DIRECT CHECK FOR DATA WITHOUT NESTED CONDITIONS ***
149+
// If we have backend data, use it as the primary source and return subscribers for NDK enhancement
150150
if (data && Array.isArray(data) && data.length > 0) {
151-
console.log(`[usePaidSubscribers] **** REAL DATA DETECTED! Bypassing all other logic ****`);
151+
console.log(`[usePaidSubscribers] Backend data detected, using as primary source`);
152152

153153
try {
154154
// Process the profiles to replace placeholder avatar URLs
155155
const processedProfiles: SubscriberProfile[] = [];
156156

157-
// Attempt to directly parse one of the data elements to verify it's structured correctly
158157
console.log(`[usePaidSubscribers] First item pubkey:`, data[0]?.pubkey);
159158
console.log(`[usePaidSubscribers] First item picture:`, data[0]?.picture);
160159

@@ -182,31 +181,26 @@ const usePaidSubscribers = (pageSize = 20) => {
182181
});
183182
}
184183

185-
console.log('[usePaidSubscribers] DIRECT STATE UPDATES WITH REAL DATA');
184+
console.log('[usePaidSubscribers] Backend data processed successfully');
186185
console.log('[usePaidSubscribers] Processed profiles count:', processedProfiles.length);
187186

188-
// Force a state update for useDummyData first
187+
// Update state with backend data
189188
setUseDummyData(false);
190-
191-
// Then update all other state with real data
192-
if (processedProfiles.length > 0) {
193-
setSubscribers(processedProfiles);
194-
}
195-
189+
setSubscribers(processedProfiles);
196190
setHasMore(data.length === pageSize);
197191
setCurrentPage(page + 1);
198192

199-
console.log('[usePaidSubscribers] State updates with real data complete!');
200-
return; // Exit early after processing real data
193+
console.log('[usePaidSubscribers] Backend data set as primary source');
194+
return; // Exit early after processing backend data
201195
} catch (processingError) {
202-
console.error('[usePaidSubscribers] Error processing profiles:', processingError);
196+
console.error('[usePaidSubscribers] Error processing backend profiles:', processingError);
203197
// Continue to fallback logic below if processing fails
204198
}
205199
}
206200

207-
// Fallback logic if the direct approach failed
201+
// Fallback logic if no backend data
208202
if (isMounted.current) {
209-
console.log('[usePaidSubscribers] Using fallback logic - probably no valid data');
203+
console.log('[usePaidSubscribers] No backend data found, using dummy data');
210204
setUseDummyData(true);
211205
setSubscribers(dummyProfiles);
212206
setHasMore(false);

src/hooks/useWalletAuth.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,17 @@ const useWalletAuth = () => {
5454

5555
console.log(challenge)
5656

57-
// Sign the challenge using Nostr
58-
const signedEvent = await window.nostr.signEvent({
57+
// Create the event to sign
58+
const event = {
5959
pubkey: npub,
6060
content: challenge,
6161
created_at: Math.floor(Date.now() / 1000),
6262
kind: 1,
6363
tags: [],
64-
});
64+
};
65+
66+
// Sign the challenge using Nostr
67+
const signedEvent = await window.nostr.signEvent(event);
6568

6669
// Send the signed challenge to the backend for verification
6770
const verifyResponse = await fetch(`${config.walletBaseURL}/verify`, {
@@ -70,7 +73,7 @@ const useWalletAuth = () => {
7073
body: JSON.stringify({
7174
challenge,
7275
signature: signedEvent.sig,
73-
messageHash: signedEvent.id,
76+
messageHash: event.content,
7477
event: signedEvent,
7578
}),
7679
});

src/types/nostr.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,5 @@ export interface NostrProvider {
1313
};
1414
}
1515

16-
declare global {
17-
interface Window {
18-
nostr?: NostrProvider;
19-
}
20-
}
16+
// Global Window declaration removed to avoid conflict with NDK types
17+
// NDK will provide the window.nostr types automatically

src/utils/utils.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,14 @@ export const getSatsCurrency = (price: number | string, currency: CurrencyTypeEn
2323
return isIcon ? `${currencySymbol}${formattedPrice}` : `${formattedPrice} ${currency}`;
2424
};
2525
export const convertNDKUserProfileToSubscriberProfile = (pubkey: string, user: NDKUserProfile): SubscriberProfile => {
26+
// Handle display_name from the profile data since NDK sometimes uses different field names
27+
const displayName = user.name ||
28+
('display_name' in user ? user.display_name : '') ||
29+
('displayName' in user ? user.displayName : '') || '';
30+
2631
return {
2732
pubkey,
28-
name: user.name || '',
33+
name: typeof displayName === 'string' ? displayName : '',
2934
picture: user.picture || '',
3035
about: user.about || '',
3136
};

0 commit comments

Comments
 (0)