1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,3 +30,4 @@ temp*build*/
|
|||||||
/.codex-temp
|
/.codex-temp
|
||||||
/target/
|
/target/
|
||||||
/logs
|
/logs
|
||||||
|
.worktrees/
|
||||||
|
|||||||
@@ -692,6 +692,7 @@ afterEach(() => {
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
value: undefined,
|
value: undefined,
|
||||||
});
|
});
|
||||||
|
window.history.replaceState(null, '', '/');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('opens wallet ledger modal from narrative coin card', async () => {
|
test('opens wallet ledger modal from narrative coin card', async () => {
|
||||||
@@ -859,6 +860,37 @@ test('profile redeem invite shortcut hides after redeemed or one day old', async
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('invite query opens login modal for logged out users', async () => {
|
||||||
|
const openLoginModal = vi.fn();
|
||||||
|
window.history.replaceState(null, '', '/?inviteCode=spring-2026');
|
||||||
|
|
||||||
|
renderLoggedOutHomeView(openLoginModal);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(openLoginModal).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('invite query opens redeem modal directly for logged in users', async () => {
|
||||||
|
window.history.replaceState(null, '', '/?inviteCode=spring-2026');
|
||||||
|
|
||||||
|
renderProfileView();
|
||||||
|
|
||||||
|
const input = await screen.findByLabelText('邀请码');
|
||||||
|
expect((input as HTMLInputElement).value).toBe('SPRING2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('profile redeem invite modal reads query invite code after login', async () => {
|
||||||
|
window.history.replaceState(null, '', '/?inviteCode=spring-2026');
|
||||||
|
|
||||||
|
renderProfileView();
|
||||||
|
|
||||||
|
const input = await screen.findByLabelText('邀请码');
|
||||||
|
|
||||||
|
expect((input as HTMLInputElement).value).toBe('SPRING2026');
|
||||||
|
});
|
||||||
|
|
||||||
test('profile redeem invite modal submits code and hides shortcut after success', async () => {
|
test('profile redeem invite modal submits code and hides shortcut after success', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
const onRechargeSuccess = vi.fn();
|
const onRechargeSuccess = vi.fn();
|
||||||
|
|||||||
@@ -62,10 +62,10 @@ import {
|
|||||||
} from '../../services/authService';
|
} from '../../services/authService';
|
||||||
import { copyTextToClipboard } from '../../services/clipboard';
|
import { copyTextToClipboard } from '../../services/clipboard';
|
||||||
import {
|
import {
|
||||||
|
claimRpgProfileTaskReward,
|
||||||
getRpgProfileReferralInviteCenter,
|
getRpgProfileReferralInviteCenter,
|
||||||
getRpgProfileTasks,
|
getRpgProfileTasks,
|
||||||
getRpgProfileWalletLedger,
|
getRpgProfileWalletLedger,
|
||||||
claimRpgProfileTaskReward,
|
|
||||||
redeemRpgProfileReferralInviteCode,
|
redeemRpgProfileReferralInviteCode,
|
||||||
redeemRpgProfileRewardCode,
|
redeemRpgProfileRewardCode,
|
||||||
} from '../../services/rpg-entry/rpgProfileClient';
|
} from '../../services/rpg-entry/rpgProfileClient';
|
||||||
@@ -158,6 +158,8 @@ const AVATAR_OUTPUT_SIZE = 256;
|
|||||||
const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
|
||||||
const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
|
const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
|
||||||
const PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS = 24 * 60 * 60 * 1000;
|
const PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS = 24 * 60 * 60 * 1000;
|
||||||
|
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
|
||||||
|
|
||||||
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
|
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
|
||||||
type MobileHomeChannel = 'recommend' | 'today' | 'category';
|
type MobileHomeChannel = 'recommend' | 'today' | 'category';
|
||||||
type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
|
type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
|
||||||
@@ -1514,6 +1516,24 @@ function isWithinProfileInviteRedeemWindow(
|
|||||||
return Date.now() - createdTime <= PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS;
|
return Date.now() - createdTime <= PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeProfileInviteQueryCode(value: string | null | undefined) {
|
||||||
|
return (value ?? '')
|
||||||
|
.trim()
|
||||||
|
.replace(/[^0-9a-z]/giu, '')
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function readProfileInviteCodeFromLocationSearch(search: string) {
|
||||||
|
const params = new URLSearchParams(search);
|
||||||
|
for (const key of PROFILE_INVITE_QUERY_KEYS) {
|
||||||
|
const inviteCode = normalizeProfileInviteQueryCode(params.get(key));
|
||||||
|
if (inviteCode) {
|
||||||
|
return inviteCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
function formatPlayedWorkType(value: string | null | undefined) {
|
function formatPlayedWorkType(value: string | null | undefined) {
|
||||||
const normalizedValue = (value ?? '').toLowerCase();
|
const normalizedValue = (value ?? '').toLowerCase();
|
||||||
if (normalizedValue === 'puzzle') {
|
if (normalizedValue === 'puzzle') {
|
||||||
@@ -2695,7 +2715,17 @@ export function RpgEntryHomeView({
|
|||||||
const [isLoadingReferral, setIsLoadingReferral] = useState(false);
|
const [isLoadingReferral, setIsLoadingReferral] = useState(false);
|
||||||
const [isReferralCenterInitialized, setIsReferralCenterInitialized] =
|
const [isReferralCenterInitialized, setIsReferralCenterInitialized] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [referralRedeemCode, setReferralRedeemCode] = useState('');
|
const pendingProfileInviteCode = useMemo(
|
||||||
|
() =>
|
||||||
|
typeof window === 'undefined'
|
||||||
|
? ''
|
||||||
|
: readProfileInviteCodeFromLocationSearch(window.location.search),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const autoOpenedInviteQueryRef = useRef(false);
|
||||||
|
const [referralRedeemCode, setReferralRedeemCode] = useState(
|
||||||
|
pendingProfileInviteCode,
|
||||||
|
);
|
||||||
const [isSubmittingReferralRedeem, setIsSubmittingReferralRedeem] =
|
const [isSubmittingReferralRedeem, setIsSubmittingReferralRedeem] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [referralError, setReferralError] = useState<string | null>(null);
|
const [referralError, setReferralError] = useState<string | null>(null);
|
||||||
@@ -2936,6 +2966,23 @@ export function RpgEntryHomeView({
|
|||||||
}
|
}
|
||||||
authUi?.openLoginModal();
|
authUi?.openLoginModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pendingProfileInviteCode || autoOpenedInviteQueryRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
autoOpenedInviteQueryRef.current = true;
|
||||||
|
if (!authUi?.user) {
|
||||||
|
authUi?.openLoginModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setReferralRedeemCode(pendingProfileInviteCode);
|
||||||
|
setReferralError(null);
|
||||||
|
setReferralSuccess(null);
|
||||||
|
setProfilePopupPanel('redeem');
|
||||||
|
}, [authUi, pendingProfileInviteCode]);
|
||||||
const scheduleProfileCopyStateReset = () => {
|
const scheduleProfileCopyStateReset = () => {
|
||||||
if (profileCopyResetTimerRef.current !== null) {
|
if (profileCopyResetTimerRef.current !== null) {
|
||||||
window.clearTimeout(profileCopyResetTimerRef.current);
|
window.clearTimeout(profileCopyResetTimerRef.current);
|
||||||
@@ -3173,7 +3220,7 @@ export function RpgEntryHomeView({
|
|||||||
setReferralError(null);
|
setReferralError(null);
|
||||||
setReferralSuccess(null);
|
setReferralSuccess(null);
|
||||||
if (panel === 'redeem') {
|
if (panel === 'redeem') {
|
||||||
setReferralRedeemCode('');
|
setReferralRedeemCode(pendingProfileInviteCode);
|
||||||
}
|
}
|
||||||
if (panel === 'community') {
|
if (panel === 'community') {
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user