1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,3 +30,4 @@ temp*build*/
|
||||
/.codex-temp
|
||||
/target/
|
||||
/logs
|
||||
.worktrees/
|
||||
|
||||
@@ -692,6 +692,7 @@ afterEach(() => {
|
||||
configurable: true,
|
||||
value: undefined,
|
||||
});
|
||||
window.history.replaceState(null, '', '/');
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
|
||||
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 () => {
|
||||
const user = userEvent.setup();
|
||||
const onRechargeSuccess = vi.fn();
|
||||
|
||||
@@ -62,10 +62,10 @@ import {
|
||||
} from '../../services/authService';
|
||||
import { copyTextToClipboard } from '../../services/clipboard';
|
||||
import {
|
||||
claimRpgProfileTaskReward,
|
||||
getRpgProfileReferralInviteCenter,
|
||||
getRpgProfileTasks,
|
||||
getRpgProfileWalletLedger,
|
||||
claimRpgProfileTaskReward,
|
||||
redeemRpgProfileReferralInviteCode,
|
||||
redeemRpgProfileRewardCode,
|
||||
} 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 PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
|
||||
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 MobileHomeChannel = 'recommend' | 'today' | 'category';
|
||||
type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
|
||||
@@ -1514,6 +1516,24 @@ function isWithinProfileInviteRedeemWindow(
|
||||
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) {
|
||||
const normalizedValue = (value ?? '').toLowerCase();
|
||||
if (normalizedValue === 'puzzle') {
|
||||
@@ -2695,7 +2715,17 @@ export function RpgEntryHomeView({
|
||||
const [isLoadingReferral, setIsLoadingReferral] = useState(false);
|
||||
const [isReferralCenterInitialized, setIsReferralCenterInitialized] =
|
||||
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] =
|
||||
useState(false);
|
||||
const [referralError, setReferralError] = useState<string | null>(null);
|
||||
@@ -2936,6 +2966,23 @@ export function RpgEntryHomeView({
|
||||
}
|
||||
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 = () => {
|
||||
if (profileCopyResetTimerRef.current !== null) {
|
||||
window.clearTimeout(profileCopyResetTimerRef.current);
|
||||
@@ -3173,7 +3220,7 @@ export function RpgEntryHomeView({
|
||||
setReferralError(null);
|
||||
setReferralSuccess(null);
|
||||
if (panel === 'redeem') {
|
||||
setReferralRedeemCode('');
|
||||
setReferralRedeemCode(pendingProfileInviteCode);
|
||||
}
|
||||
if (panel === 'community') {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user