注册时自动使用链接邀请码

手机号注册提交链接中的邀请码。

系统邀请码 metadata.tags 会合并到账户标签。

更新自动兑换邀请码的前端与模块测试。
This commit is contained in:
2026-07-01 17:48:01 +08:00
parent ddbcd29b49
commit 71a6ed5945
3 changed files with 41 additions and 26 deletions
@@ -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<Vec<Str
let tags = metadata
.get("userTags")
.or_else(|| metadata.get("user_tags"))
.or_else(|| metadata.get("tags"))
.and_then(JsonValue::as_array)
.map(|items| {
items
+12 -23
View File
@@ -636,7 +636,7 @@ test('phone login result is not overwritten by an older guest hydrate', async ()
expect(screen.queryByRole('dialog', { name: '账号入口' })).toBeNull();
});
test('auth gate hides register entry and opens invite modal for new sms account', async () => {
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 () => {
+18 -3
View File
@@ -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(