diff --git a/server-rs/crates/spacetime-module/src/runtime/profile.rs b/server-rs/crates/spacetime-module/src/runtime/profile.rs index d8adf26c8..462cf0d78 100644 --- a/server-rs/crates/spacetime-module/src/runtime/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/profile.rs @@ -1631,6 +1631,16 @@ mod tests { assert!(result.error_message.is_none()); } + #[test] + fn invite_code_metadata_tags_are_user_tags() { + let tags = profile_invite_code_metadata_user_tags( + r#"{"tags":["系统"," beta ","系统"],"channel":"spring"}"#, + ) + .expect("metadata tags should parse"); + + assert_eq!(tags, vec!["系统".to_string(), "beta".to_string()]); + } + #[test] fn recent_public_work_play_counts_group_requested_profiles_in_window() { let now_micros = PUBLIC_WORK_PLAY_DAY_MICROS * 10; @@ -2801,6 +2811,7 @@ fn profile_invite_code_metadata_user_tags(metadata_json: &str) -> Result { +test('auth gate auto redeems link invite code for new sms account', async () => { const user = userEvent.setup(); window.history.replaceState(null, '', '/?inviteCode=spring-2026'); authMocks.getAuthLoginOptions.mockResolvedValue({ @@ -646,7 +646,14 @@ test('auth gate hides register entry and opens invite modal for new sms account' token: 'jwt-phone-new', user: mockUser, created: true, - referral: null, + referral: { + ok: true, + message: '邀请码已绑定', + inviteeRewardGranted: true, + inviterRewardGranted: false, + inviteeBalanceAfter: 30, + inviterBalanceAfter: 0, + }, }); render( @@ -674,30 +681,12 @@ test('auth gate hides register entry and opens invite modal for new sms account' expect(authMocks.loginWithPhoneCode).toHaveBeenCalledWith( '13800000000', '123456', - ); - }); - - const inviteDialog = await screen.findByRole('dialog', { - name: '请填写邀请码', - }); - expect(inviteDialog.className).toContain('platform-auth-card'); - expect(inviteDialog.className).toContain('platform-modal-shell'); - const inviteCodeInput = within(inviteDialog).getByLabelText( - '邀请码', - ) as HTMLInputElement; - expect(inviteCodeInput.value).toBe('SPRING2026'); - expect(inviteCodeInput.className).toContain('platform-text-field'); - expect( - within(inviteDialog).getByRole('button', { name: '提交' }), - ).toBeTruthy(); - - await user.click(within(inviteDialog).getByRole('button', { name: '提交' })); - - await waitFor(() => { - expect(authMocks.redeemRegistrationInviteCode).toHaveBeenCalledWith( 'SPRING2026', ); }); + + expect(screen.queryByRole('dialog', { name: '请填写邀请码' })).toBeNull(); + expect(authMocks.redeemRegistrationInviteCode).not.toHaveBeenCalled(); }); test('registration invite modal can skip when invite code is empty', async () => { diff --git a/src/components/auth/AuthGate.tsx b/src/components/auth/AuthGate.tsx index 715c72586..80331e5a5 100644 --- a/src/components/auth/AuthGate.tsx +++ b/src/components/auth/AuthGate.tsx @@ -966,11 +966,26 @@ export function AuthGate({ children }: AuthGateProps) { setLoggingIn(true); setError(''); try { - const response = await loginWithPhoneCode(phone, code); + const registrationInviteCode = + pendingInviteCode || readInviteCodeFromLocation(); + const response = registrationInviteCode + ? await loginWithPhoneCode(phone, code, registrationInviteCode) + : await loginWithPhoneCode(phone, code); + const autoRedeemedInvite = response.referral?.ok === true; setStoredLastLoginPhone(phone); setLoginCaptchaChallenge(null); - setShowRegistrationInviteModal(response.created); - setRegistrationInviteError(''); + setShowRegistrationInviteModal( + response.created && !autoRedeemedInvite, + ); + setRegistrationInviteError( + response.referral?.ok === false + ? (response.referral.message ?? + '邀请码绑定失败,请确认后重试。') + : '', + ); + if (autoRedeemedInvite) { + setPendingInviteCode(''); + } activateReadyUser(response.user); } catch (loginError) { setError(