Add Matches page, OkCupid integration, and major UI/feature updates
- New Matches page with match scoring system - New OkCupid page and API integration - Enhanced Likes page with scanner improvements and enrichment - Updated Settings, Discover, Messages, and Chat pages - Improved auth, GraphQL client, and Stream Chat setup - Added new backend endpoints (matchScoring.js) - Removed old Proxyman capture logs - Updated nginx config and Vite proxy settings Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+620
-60
@@ -1,8 +1,7 @@
|
||||
import { useQuery, useMutation } from '@apollo/client/react';
|
||||
import { DISCOVER_SEARCH_SETTINGS_QUERY, IS_INCOGNITO_QUERY } from '../api/operations/queries';
|
||||
import { DEVICE_LOCATION_UPDATE_MUTATION, SEARCH_SETTINGS_UPDATE_MUTATION } from '../api/operations/mutations';
|
||||
import { TEST_CREDENTIALS, getCredentials, setCredentials, clearCredentials } from '../config/constants';
|
||||
import { saveCredentials as syncCredentialsToServer, saveCustomLocation } from '../api/dataSync';
|
||||
import { DISCOVER_SEARCH_SETTINGS_QUERY, IS_INCOGNITO_QUERY, POPULAR_LOCATIONS_QUERY, APP_SETTINGS_QUERY, REDEEMED_OFFERS_QUERY, HAS_LINKED_REFLECTION_QUERY } from '../api/operations/queries';
|
||||
import { DEVICE_LOCATION_UPDATE_MUTATION, SEARCH_SETTINGS_UPDATE_MUTATION, PROFILE_LOCATION_UPDATE_MUTATION, APP_SETTINGS_UPDATE_MUTATION, SYNC_ACCOUNT_MUTATION, ACCOUNT_REDEEM_OFFER_MUTATION, ACCOUNT_DEACTIVATE_MUTATION, ACCOUNT_TERMINATE_MUTATION } from '../api/operations/mutations';
|
||||
import { saveCustomLocation } from '../api/dataSync';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
import { authManager } from '../api/auth';
|
||||
import type { AuthStatus } from '../api/auth';
|
||||
@@ -220,10 +219,9 @@ export function SettingsPage() {
|
||||
const [tempDesiringFor, setTempDesiringFor] = useState<string[]>([]);
|
||||
const [savingDesiringFor, setSavingDesiringFor] = useState(false);
|
||||
|
||||
// Auth credentials state
|
||||
const currentCreds = getCredentials();
|
||||
const [profileIdInput, setProfileIdInput] = useState(currentCreds.PROFILE_ID);
|
||||
const [refreshTokenInput, setRefreshTokenInput] = useState(currentCreds.REFRESH_TOKEN);
|
||||
// Auth credentials state — backend is single source of truth
|
||||
const [profileIdInput, setProfileIdInput] = useState(authManager.getProfileId());
|
||||
const [refreshTokenInput, setRefreshTokenInput] = useState('');
|
||||
const [authStatus, setAuthStatus] = useState<AuthStatus>(authManager.getStatus());
|
||||
const [authSaving, setAuthSaving] = useState(false);
|
||||
const [authMessage, setAuthMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
@@ -268,29 +266,160 @@ export function SettingsPage() {
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Seed token to backend on load
|
||||
// Load current profile ID from backend on mount
|
||||
useEffect(() => {
|
||||
const seedToken = async () => {
|
||||
const creds = getCredentials();
|
||||
if (creds.REFRESH_TOKEN && creds.PROFILE_ID) {
|
||||
try {
|
||||
await fetch('/api/location-rotation/seed-token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
refreshToken: creds.REFRESH_TOKEN,
|
||||
profileId: creds.PROFILE_ID,
|
||||
analyticsId: creds.EVENT_ANALYTICS_ID,
|
||||
}),
|
||||
});
|
||||
} catch (e) {}
|
||||
}
|
||||
};
|
||||
seedToken();
|
||||
setProfileIdInput(authManager.getProfileId());
|
||||
}, []);
|
||||
|
||||
const [rotationError, setRotationError] = useState<string | null>(null);
|
||||
|
||||
// Teleport state
|
||||
const [teleportLoading, setTeleportLoading] = useState(false);
|
||||
const [teleportStatus, setTeleportStatus] = useState<string | null>(null);
|
||||
const [teleportCity, setTeleportCity] = useState<string | null>(null);
|
||||
|
||||
// Notifications/Preferences state
|
||||
const [syncingAccount, setSyncingAccount] = useState(false);
|
||||
const [syncResult, setSyncResult] = useState<string | null>(null);
|
||||
|
||||
// Account management state
|
||||
const [offerCode, setOfferCode] = useState('');
|
||||
const [redeemingOffer, setRedeemingOffer] = useState(false);
|
||||
const [offerResult, setOfferResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
const [pauseConfirm, setPauseConfirm] = useState(false);
|
||||
const [pausingAccount, setPausingAccount] = useState(false);
|
||||
const [deleteConfirm, setDeleteConfirm] = useState(0); // 0=none, 1=first confirm, 2=second confirm
|
||||
const [deletingAccount, setDeletingAccount] = useState(false);
|
||||
const [accountActionResult, setAccountActionResult] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
// Teleport queries/mutations
|
||||
const { data: popularLocationsData } = useQuery(POPULAR_LOCATIONS_QUERY);
|
||||
const [profileLocationUpdate] = useMutation(PROFILE_LOCATION_UPDATE_MUTATION);
|
||||
|
||||
// App settings queries/mutations
|
||||
const { data: appSettingsData, refetch: refetchAppSettings } = useQuery(APP_SETTINGS_QUERY);
|
||||
const [appSettingsUpdate] = useMutation(APP_SETTINGS_UPDATE_MUTATION);
|
||||
const [syncAccount] = useMutation(SYNC_ACCOUNT_MUTATION);
|
||||
|
||||
// Account queries/mutations
|
||||
const { data: redeemedOffersData, refetch: refetchOffers } = useQuery(REDEEMED_OFFERS_QUERY);
|
||||
const { data: linkedReflectionData } = useQuery(HAS_LINKED_REFLECTION_QUERY);
|
||||
const [redeemOffer] = useMutation(ACCOUNT_REDEEM_OFFER_MUTATION);
|
||||
const [deactivateAccount] = useMutation(ACCOUNT_DEACTIVATE_MUTATION);
|
||||
const [terminateAccount] = useMutation(ACCOUNT_TERMINATE_MUTATION);
|
||||
|
||||
const handleTeleport = async (loc: any) => {
|
||||
setTeleportLoading(true);
|
||||
setTeleportStatus(null);
|
||||
try {
|
||||
await profileLocationUpdate({
|
||||
variables: {
|
||||
input: {
|
||||
teleportLocation: {
|
||||
latitude: loc.latitude,
|
||||
longitude: loc.longitude,
|
||||
city: loc.geocode?.city || '',
|
||||
country: loc.geocode?.country || '',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setTeleportCity(`${loc.geocode?.city || 'Unknown'}, ${loc.geocode?.country || ''}`);
|
||||
setTeleportStatus('success');
|
||||
} catch (err) {
|
||||
setTeleportStatus('error');
|
||||
} finally {
|
||||
setTeleportLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetTeleport = async () => {
|
||||
setTeleportLoading(true);
|
||||
setTeleportStatus(null);
|
||||
try {
|
||||
await profileLocationUpdate({
|
||||
variables: {
|
||||
input: {
|
||||
deviceLocation: { latitude: 0, longitude: 0 },
|
||||
},
|
||||
},
|
||||
});
|
||||
setTeleportCity(null);
|
||||
setTeleportStatus('reset');
|
||||
} catch (err) {
|
||||
setTeleportStatus('error');
|
||||
} finally {
|
||||
setTeleportLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleNotification = async (field: string, currentValue: boolean) => {
|
||||
try {
|
||||
await appSettingsUpdate({ variables: { [field]: !currentValue } });
|
||||
refetchAppSettings();
|
||||
} catch (err) {
|
||||
console.error('Failed to update setting:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSyncAccount = async () => {
|
||||
setSyncingAccount(true);
|
||||
setSyncResult(null);
|
||||
try {
|
||||
const result = await syncAccount();
|
||||
const data = result.data?.syncAccount;
|
||||
setSyncResult(`Synced: Majestic=${data?.isMajestic ? 'Yes' : 'No'}, Uplift=${data?.isUplift ? 'Yes' : 'No'}, Pings=${data?.availablePings ?? 'N/A'}`);
|
||||
} catch (err) {
|
||||
setSyncResult('Sync failed: ' + (err instanceof Error ? err.message : 'Unknown error'));
|
||||
} finally {
|
||||
setSyncingAccount(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRedeemOffer = async () => {
|
||||
if (!offerCode.trim()) return;
|
||||
setRedeemingOffer(true);
|
||||
setOfferResult(null);
|
||||
try {
|
||||
await redeemOffer({ variables: { input: { offerName: offerCode.trim() } } });
|
||||
setOfferResult({ type: 'success', text: `Offer "${offerCode.trim()}" redeemed!` });
|
||||
setOfferCode('');
|
||||
refetchOffers();
|
||||
} catch (err) {
|
||||
setOfferResult({ type: 'error', text: err instanceof Error ? err.message : 'Failed to redeem offer' });
|
||||
} finally {
|
||||
setRedeemingOffer(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePauseAccount = async () => {
|
||||
setPausingAccount(true);
|
||||
setAccountActionResult(null);
|
||||
try {
|
||||
await deactivateAccount();
|
||||
setAccountActionResult({ type: 'success', text: 'Account paused (deactivated).' });
|
||||
setPauseConfirm(false);
|
||||
} catch (err) {
|
||||
setAccountActionResult({ type: 'error', text: err instanceof Error ? err.message : 'Failed to pause account' });
|
||||
} finally {
|
||||
setPausingAccount(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
setDeletingAccount(true);
|
||||
setAccountActionResult(null);
|
||||
try {
|
||||
await terminateAccount();
|
||||
setAccountActionResult({ type: 'success', text: 'Account terminated. You will be logged out.' });
|
||||
setDeleteConfirm(0);
|
||||
} catch (err) {
|
||||
setAccountActionResult({ type: 'error', text: err instanceof Error ? err.message : 'Failed to delete account' });
|
||||
} finally {
|
||||
setDeletingAccount(false);
|
||||
}
|
||||
};
|
||||
|
||||
const updateRotation = async (updates: Record<string, any>) => {
|
||||
setRotationError(null);
|
||||
try {
|
||||
@@ -388,31 +517,34 @@ export function SettingsPage() {
|
||||
const profileId = profileIdInput.trim();
|
||||
const refreshToken = refreshTokenInput.trim();
|
||||
|
||||
// Save to localStorage
|
||||
setCredentials({ profileId, refreshToken });
|
||||
if (!refreshToken) {
|
||||
setAuthMessage({ type: 'error', text: 'Refresh token is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync to server for cross-browser persistence
|
||||
await syncCredentialsToServer(profileId, refreshToken);
|
||||
// Seed directly to backend — single source of truth
|
||||
const resp = await fetch('/api/auth/seed', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refreshToken, profileId }),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
|
||||
await authManager.forceRefresh();
|
||||
setAuthMessage({ type: 'success', text: 'Credentials saved and synced to server!' });
|
||||
// Reload page to apply new credentials everywhere
|
||||
setAuthMessage({ type: 'success', text: 'Credentials saved to server! All devices will use the new token.' });
|
||||
setRefreshTokenInput('');
|
||||
setTimeout(() => window.location.reload(), 1500);
|
||||
} catch (err) {
|
||||
setAuthMessage({ type: 'error', text: err instanceof Error ? err.message : 'Failed to refresh token' });
|
||||
setAuthMessage({ type: 'error', text: err instanceof Error ? err.message : 'Failed to save credentials' });
|
||||
} finally {
|
||||
setAuthSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetCredentials = () => {
|
||||
clearCredentials();
|
||||
const defaultCreds = getCredentials();
|
||||
setProfileIdInput(defaultCreds.PROFILE_ID);
|
||||
setRefreshTokenInput(defaultCreds.REFRESH_TOKEN);
|
||||
setAuthMessage({ type: 'success', text: 'Credentials reset to defaults. Reload to apply.' });
|
||||
};
|
||||
|
||||
const handleRefreshToken = async () => {
|
||||
setAuthSaving(true);
|
||||
setAuthMessage(null);
|
||||
@@ -438,19 +570,20 @@ export function SettingsPage() {
|
||||
const { data: settingsData, loading: settingsLoading } = useQuery(
|
||||
DISCOVER_SEARCH_SETTINGS_QUERY,
|
||||
{
|
||||
variables: { profileId: TEST_CREDENTIALS.PROFILE_ID },
|
||||
variables: { profileId: authManager.getProfileId() },
|
||||
}
|
||||
);
|
||||
|
||||
const { data: incognitoData, loading: incognitoLoading } = useQuery(
|
||||
IS_INCOGNITO_QUERY,
|
||||
{
|
||||
variables: { profileId: TEST_CREDENTIALS.PROFILE_ID },
|
||||
variables: { profileId: authManager.getProfileId() },
|
||||
}
|
||||
);
|
||||
|
||||
const [updateLocation] = useMutation(DEVICE_LOCATION_UPDATE_MUTATION);
|
||||
const [updateSearchSettings] = useMutation(SEARCH_SETTINGS_UPDATE_MUTATION);
|
||||
const [locationStatus, setLocationStatus] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
if (settingsLoading || incognitoLoading) return <LoadingPage message="Loading settings..." />;
|
||||
|
||||
@@ -483,6 +616,8 @@ export function SettingsPage() {
|
||||
},
|
||||
});
|
||||
|
||||
setLocationStatus({ type: 'success', text: `Location set to ${result.displayName} — you'll appear to users in this area` });
|
||||
setTimeout(() => setLocationStatus(null), 4000);
|
||||
setSearchQuery('');
|
||||
} else {
|
||||
setSearchError('Location not found. Try a different search.');
|
||||
@@ -501,9 +636,10 @@ export function SettingsPage() {
|
||||
name: saved.name,
|
||||
};
|
||||
setLocation(newLocation);
|
||||
setLocationStatus(null);
|
||||
|
||||
try {
|
||||
const result = await updateLocation({
|
||||
await updateLocation({
|
||||
variables: {
|
||||
input: {
|
||||
latitude: saved.latitude,
|
||||
@@ -511,15 +647,13 @@ export function SettingsPage() {
|
||||
},
|
||||
},
|
||||
});
|
||||
console.log('DeviceLocationUpdate response:', result.data);
|
||||
|
||||
// Check if API actually updated the location
|
||||
const deviceLocation = result.data?.deviceLocationUpdate?.location?.device;
|
||||
if (deviceLocation && (deviceLocation.latitude === 0 && deviceLocation.longitude === 0)) {
|
||||
console.warn('API returned 0,0 - location update may require premium/Majestic membership');
|
||||
}
|
||||
// API returns 0,0 as privacy masking — location IS set server-side
|
||||
setLocationStatus({ type: 'success', text: `Location set to ${saved.name} — you'll appear to users in this area` });
|
||||
setTimeout(() => setLocationStatus(null), 4000);
|
||||
} catch (error) {
|
||||
console.error('Failed to update location:', error);
|
||||
setLocationStatus({ type: 'error', text: 'Failed to update location' });
|
||||
setTimeout(() => setLocationStatus(null), 4000);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -742,6 +876,22 @@ export function SettingsPage() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Location status toast */}
|
||||
{locationStatus && (
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '10px 16px',
|
||||
borderRadius: '10px',
|
||||
fontSize: '13px',
|
||||
fontFamily: "'Satoshi', sans-serif",
|
||||
background: locationStatus.type === 'success' ? 'rgba(34,197,94,0.15)' : 'rgba(239,68,68,0.15)',
|
||||
color: locationStatus.type === 'success' ? '#86efac' : '#f87171',
|
||||
border: `1px solid ${locationStatus.type === 'success' ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
|
||||
}}>
|
||||
{locationStatus.text}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1263,6 +1413,422 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Teleport */}
|
||||
<div style={styles.card}>
|
||||
<div style={styles.cardContent}>
|
||||
<div style={styles.sectionHeader}>
|
||||
<div style={styles.iconBox('#8b5cf6')}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 21a9.004 9.004 0 008.716-6.747M12 21a9.004 9.004 0 01-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 017.843 4.582M12 3a8.997 8.997 0 00-7.843 4.582m15.686 0A11.953 11.953 0 0112 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0121 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0112 16.5a17.92 17.92 0 01-8.716-2.247m0 0A8.966 8.966 0 013 12c0-1.97.633-3.794 1.706-5.277" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style={styles.sectionTitle}>Teleport</h2>
|
||||
</div>
|
||||
|
||||
{/* Teleport Status Badge */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={styles.label}>Current Status</label>
|
||||
{teleportCity ? (
|
||||
<span style={{ ...styles.badge('primary'), fontSize: '14px' }}>
|
||||
Teleporting to {teleportCity}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{ ...styles.badge('success'), fontSize: '14px' }}>
|
||||
Device Location
|
||||
</span>
|
||||
)}
|
||||
{teleportStatus === 'error' && (
|
||||
<p style={{ color: '#ef4444', fontSize: '13px', marginTop: '8px' }}>Failed to update teleport location.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Popular Locations Grid */}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<label style={styles.label}>Popular Locations</label>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '8px' }}>
|
||||
{(popularLocationsData?.popularLocations || []).map((loc: any, i: number) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => handleTeleport(loc)}
|
||||
disabled={teleportLoading}
|
||||
style={{
|
||||
padding: '12px 10px',
|
||||
background: '#24242f',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: '10px',
|
||||
color: '#ffffff',
|
||||
fontSize: '13px',
|
||||
fontWeight: 500,
|
||||
cursor: teleportLoading ? 'wait' : 'pointer',
|
||||
opacity: teleportLoading ? 0.6 : 1,
|
||||
transition: 'all 0.2s',
|
||||
textAlign: 'center' as const,
|
||||
}}
|
||||
onMouseEnter={(e) => { if (!teleportLoading) { e.currentTarget.style.background = '#2e2e3a'; e.currentTarget.style.borderColor = 'rgba(139, 92, 246, 0.4)'; } }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.background = '#24242f'; e.currentTarget.style.borderColor = 'rgba(255,255,255,0.1)'; }}
|
||||
>
|
||||
<div style={{ fontWeight: 600, marginBottom: '2px' }}>{loc.geocode?.city || 'Unknown'}</div>
|
||||
<div style={{ fontSize: '11px', color: '#6b7280' }}>{loc.geocode?.country || ''}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{teleportLoading && (
|
||||
<p style={{ color: '#8b5cf6', fontSize: '13px', marginTop: '12px' }}>Teleporting...</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reset to Device Location */}
|
||||
<button
|
||||
onClick={handleResetTeleport}
|
||||
disabled={teleportLoading}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 24px',
|
||||
background: '#24242f',
|
||||
border: '1px solid rgba(255,255,255,0.1)',
|
||||
borderRadius: '12px',
|
||||
color: '#ffffff',
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
cursor: teleportLoading ? 'wait' : 'pointer',
|
||||
opacity: teleportLoading ? 0.6 : 1,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
>
|
||||
{teleportLoading ? 'Resetting...' : 'Reset to Device Location'}
|
||||
</button>
|
||||
|
||||
<p style={styles.note}>
|
||||
Teleport your profile to appear in a different city. Popular locations are provided by Feeld. Reset to return to your device location.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Notifications & Preferences */}
|
||||
<div style={styles.card}>
|
||||
<div style={styles.cardContent}>
|
||||
<div style={styles.sectionHeader}>
|
||||
<div style={styles.iconBox('#f97316')}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#f97316" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style={styles.sectionTitle}>Notifications & Preferences</h2>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const acct = appSettingsData?.account;
|
||||
const appSettings = acct?.appSettings;
|
||||
if (!appSettings) return <p style={{ color: '#6b7280' }}>Loading notification settings...</p>;
|
||||
|
||||
const toggles: Array<{ label: string; field: string; value: boolean }> = [
|
||||
{ label: 'New Connection', field: 'receiveNewConnectionPushNotifications', value: !!appSettings.receiveNewConnectionPushNotifications },
|
||||
{ label: 'New Ping', field: 'receiveNewPingPushNotifications', value: !!appSettings.receiveNewPingPushNotifications },
|
||||
{ label: 'New Message', field: 'receiveNewMessagePushNotifications', value: !!appSettings.receiveNewMessagePushNotifications },
|
||||
{ label: 'New Like', field: 'receiveNewLikePushNotifications', value: !!appSettings.receiveNewLikePushNotifications },
|
||||
{ label: 'Marketing', field: 'receiveMarketingNotifications', value: !!appSettings.receiveMarketingNotifications },
|
||||
{ label: 'News Email', field: 'receiveNewsEmailNotifications', value: !!appSettings.receiveNewsEmailNotifications },
|
||||
{ label: 'Promotions Email', field: 'receivePromotionsEmailNotifications', value: !!appSettings.receivePromotionsEmailNotifications },
|
||||
{ label: 'News Push', field: 'receiveNewsPushNotifications', value: !!appSettings.receiveNewsPushNotifications },
|
||||
{ label: 'Promotions Push', field: 'receivePromotionsPushNotifications', value: !!appSettings.receivePromotionsPushNotifications },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '24px' }}>
|
||||
{toggles.map(({ label, field, value }) => (
|
||||
<div
|
||||
key={field}
|
||||
style={{ ...styles.toggleContainer, cursor: 'pointer' }}
|
||||
onClick={() => handleToggleNotification(field, value)}
|
||||
>
|
||||
<div>
|
||||
<p style={{ color: '#ffffff', fontWeight: 500, margin: 0 }}>{label}</p>
|
||||
</div>
|
||||
<div style={{ ...styles.toggle(value), cursor: 'pointer' }}>
|
||||
<div style={styles.toggleKnob(value)} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Distance Units */}
|
||||
<div
|
||||
style={{ ...styles.toggleContainer, cursor: 'pointer', marginBottom: '24px' }}
|
||||
onClick={() => handleToggleNotification('isDistanceInMiles', !!acct?.isDistanceInMiles)}
|
||||
>
|
||||
<div>
|
||||
<p style={{ color: '#ffffff', fontWeight: 500, margin: 0 }}>Distance in Miles</p>
|
||||
<p style={{ color: '#6b7280', fontSize: '13px', margin: '4px 0 0 0' }}>
|
||||
{acct?.isDistanceInMiles ? 'Using miles' : 'Using kilometers'}
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ ...styles.toggle(!!acct?.isDistanceInMiles), cursor: 'pointer' }}>
|
||||
<div style={styles.toggleKnob(!!acct?.isDistanceInMiles)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sync Account */}
|
||||
<button
|
||||
onClick={handleSyncAccount}
|
||||
disabled={syncingAccount}
|
||||
style={{
|
||||
...styles.button,
|
||||
width: '100%',
|
||||
background: 'linear-gradient(135deg, #f97316 0%, #ea580c 100%)',
|
||||
opacity: syncingAccount ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{syncingAccount ? 'Syncing...' : 'Sync Account'}
|
||||
</button>
|
||||
{syncResult && (
|
||||
<p style={{ color: '#f97316', fontSize: '13px', marginTop: '12px' }}>{syncResult}</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
<p style={styles.note}>
|
||||
Toggle notification preferences for your account. Changes are saved immediately. Sync Account refreshes your subscription status from the server.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Account Management */}
|
||||
<div style={styles.card}>
|
||||
<div style={styles.cardContent}>
|
||||
<div style={styles.sectionHeader}>
|
||||
<div style={styles.iconBox('#06b6d4')}>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#06b6d4" strokeWidth="1.5" style={{ width: '24px', height: '24px' }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 style={styles.sectionTitle}>Account</h2>
|
||||
</div>
|
||||
|
||||
{/* Subscription Status */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label style={styles.label}>Subscription</label>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
||||
<span style={styles.badge(appSettingsData?.account?.isMajestic ? 'success' : 'default')}>
|
||||
{appSettingsData?.account?.isMajestic ? 'Majestic' : 'Free'}
|
||||
</span>
|
||||
{appSettingsData?.account?.isUplift && (
|
||||
<span style={styles.badge('primary')}>Uplift Active</span>
|
||||
)}
|
||||
<span style={styles.badge('default')}>
|
||||
{appSettingsData?.account?.availablePings ?? '?'} Pings Available
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reflect Link Status */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label style={styles.label}>Reflect Link</label>
|
||||
<span style={styles.badge(linkedReflectionData?.hasLinkedReflection ? 'success' : 'default')}>
|
||||
{linkedReflectionData?.hasLinkedReflection ? 'Linked' : 'Not Linked'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Redeemed Offers */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label style={styles.label}>Redeemed Offers</label>
|
||||
{(redeemedOffersData?.account?.redeemedOffers || []).length > 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||
{(redeemedOffersData?.account?.redeemedOffers || []).map((offer: any, i: number) => (
|
||||
<div key={i} style={styles.infoBox}>
|
||||
<p style={{ color: '#ffffff', fontWeight: 500, margin: 0 }}>{offer.offerName}</p>
|
||||
{offer.redeemedAt && (
|
||||
<p style={{ color: '#6b7280', fontSize: '12px', margin: '4px 0 0 0' }}>
|
||||
Redeemed: {new Date(offer.redeemedAt).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p style={{ color: '#6b7280', fontSize: '14px' }}>No offers redeemed</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Redeem Offer */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<label style={styles.label}>Redeem Offer Code</label>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<input
|
||||
type="text"
|
||||
value={offerCode}
|
||||
onChange={(e) => setOfferCode(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleRedeemOffer()}
|
||||
placeholder="Enter offer code..."
|
||||
style={styles.input}
|
||||
/>
|
||||
<button
|
||||
onClick={handleRedeemOffer}
|
||||
disabled={redeemingOffer || !offerCode.trim()}
|
||||
style={{
|
||||
...styles.button,
|
||||
opacity: (redeemingOffer || !offerCode.trim()) ? 0.6 : 1,
|
||||
}}
|
||||
>
|
||||
{redeemingOffer ? 'Redeeming...' : 'Redeem'}
|
||||
</button>
|
||||
</div>
|
||||
{offerResult && (
|
||||
<p style={{
|
||||
color: offerResult.type === 'success' ? '#22c55e' : '#ef4444',
|
||||
fontSize: '13px',
|
||||
marginTop: '8px',
|
||||
}}>
|
||||
{offerResult.text}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pause Account */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
{!pauseConfirm ? (
|
||||
<button
|
||||
onClick={() => setPauseConfirm(true)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 24px',
|
||||
background: 'rgba(245, 158, 11, 0.15)',
|
||||
border: '1px solid rgba(245, 158, 11, 0.3)',
|
||||
borderRadius: '12px',
|
||||
color: '#f59e0b',
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
Pause Account
|
||||
</button>
|
||||
) : (
|
||||
<div style={{ ...styles.infoBox, border: '1px solid rgba(245, 158, 11, 0.3)' }}>
|
||||
<p style={{ color: '#f59e0b', fontWeight: 500, marginBottom: '12px' }}>
|
||||
Are you sure you want to pause your account? Your profile will be hidden.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
onClick={handlePauseAccount}
|
||||
disabled={pausingAccount}
|
||||
style={{
|
||||
...styles.button,
|
||||
flex: 1,
|
||||
background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
opacity: pausingAccount ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{pausingAccount ? 'Pausing...' : 'Yes, Pause'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPauseConfirm(false)}
|
||||
style={{ ...styles.buttonSecondary, flex: 1 }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Delete Account */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
{deleteConfirm === 0 && (
|
||||
<button
|
||||
onClick={() => setDeleteConfirm(1)}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '14px 24px',
|
||||
background: 'rgba(239, 68, 68, 0.15)',
|
||||
border: '1px solid rgba(239, 68, 68, 0.3)',
|
||||
borderRadius: '12px',
|
||||
color: '#ef4444',
|
||||
fontSize: '15px',
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.2s',
|
||||
}}
|
||||
>
|
||||
Delete Account
|
||||
</button>
|
||||
)}
|
||||
{deleteConfirm === 1 && (
|
||||
<div style={{ ...styles.infoBox, border: '1px solid rgba(239, 68, 68, 0.3)' }}>
|
||||
<p style={{ color: '#ef4444', fontWeight: 500, marginBottom: '12px' }}>
|
||||
This will permanently delete your account. This action cannot be undone. Are you sure?
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
onClick={() => setDeleteConfirm(2)}
|
||||
style={{
|
||||
...styles.button,
|
||||
flex: 1,
|
||||
background: 'rgba(239, 68, 68, 0.3)',
|
||||
}}
|
||||
>
|
||||
Yes, I'm sure
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteConfirm(0)}
|
||||
style={{ ...styles.buttonSecondary, flex: 1 }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{deleteConfirm === 2 && (
|
||||
<div style={{ ...styles.infoBox, border: '2px solid #ef4444' }}>
|
||||
<p style={{ color: '#ef4444', fontWeight: 700, marginBottom: '12px', fontSize: '16px' }}>
|
||||
FINAL WARNING: This is irreversible. Your account, matches, and messages will be permanently deleted.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
onClick={handleDeleteAccount}
|
||||
disabled={deletingAccount}
|
||||
style={{
|
||||
...styles.button,
|
||||
flex: 1,
|
||||
background: 'linear-gradient(135deg, #dc2626 0%, #ef4444 100%)',
|
||||
opacity: deletingAccount ? 0.7 : 1,
|
||||
}}
|
||||
>
|
||||
{deletingAccount ? 'Deleting...' : 'DELETE PERMANENTLY'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeleteConfirm(0)}
|
||||
style={{ ...styles.buttonSecondary, flex: 1 }}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Account Action Result */}
|
||||
{accountActionResult && (
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
borderRadius: '10px',
|
||||
background: accountActionResult.type === 'success' ? 'rgba(34, 197, 94, 0.15)' : 'rgba(239, 68, 68, 0.15)',
|
||||
border: `1px solid ${accountActionResult.type === 'success' ? 'rgba(34, 197, 94, 0.3)' : 'rgba(239, 68, 68, 0.3)'}`,
|
||||
color: accountActionResult.type === 'success' ? '#22c55e' : '#ef4444',
|
||||
fontSize: '14px',
|
||||
}}>
|
||||
{accountActionResult.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p style={styles.note}>
|
||||
Pausing hides your profile temporarily. Deleting permanently removes your account, all data, matches, and messages.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auth Credentials */}
|
||||
<div style={styles.card}>
|
||||
<div style={styles.cardContent}>
|
||||
@@ -1314,15 +1880,15 @@ export function SettingsPage() {
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<label style={{ fontSize: '10px', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Profile ID</label>
|
||||
<p style={{ color: '#9ca3af', fontSize: '11px', fontFamily: 'monospace', margin: '4px 0 0 0', wordBreak: 'break-all' }}>
|
||||
{currentCreds.PROFILE_ID}
|
||||
{authManager.getProfileId()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Current Refresh Token */}
|
||||
{/* Refresh Token (managed by backend) */}
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<label style={{ fontSize: '10px', color: '#6b7280', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Refresh Token</label>
|
||||
<p style={{ color: '#9ca3af', fontSize: '10px', fontFamily: 'monospace', margin: '4px 0 0 0', wordBreak: 'break-all', maxHeight: '60px', overflow: 'auto' }}>
|
||||
{currentCreds.REFRESH_TOKEN}
|
||||
Managed by backend server
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1391,12 +1957,6 @@ export function SettingsPage() {
|
||||
>
|
||||
{authSaving ? 'Saving...' : 'Save & Refresh Token'}
|
||||
</button>
|
||||
<button
|
||||
style={styles.buttonSecondary}
|
||||
onClick={handleResetCredentials}
|
||||
>
|
||||
Reset to Defaults
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Status Message */}
|
||||
|
||||
Reference in New Issue
Block a user