fix(auth): show user agreement before login actions
CI / verify (pull_request) Has been cancelled

Quick login required agreement without a checkbox; move the consent row above CTAs across partner/shop/user/mini login pages so it stays visible on short screens.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-17 10:20:32 +08:00
parent 55e91158b3
commit 14b867a3a5
10 changed files with 235 additions and 117 deletions
+42 -16
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState, type RefObject } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
import { WECHAT_INAPP_REQUIRED_MSG } from '@dukang/weixin-sdk';
@@ -24,6 +24,32 @@ import { isWechatEnv } from '../lib/weixin';
const REMEMBER_PHONE_KEY = 'partner_remember_phone';
const REMEMBER_FLAG_KEY = 'partner_remember_account';
function AgreementCheckbox({
agreed,
onChange,
inputRef,
}: {
agreed: boolean;
onChange: (next: boolean) => void;
inputRef?: RefObject<HTMLLabelElement | null>;
}) {
return (
<label className="partner-checkbox-row partner-checkbox-row--agreement" ref={inputRef}>
<input type="checkbox" checked={agreed} onChange={(e) => onChange(e.target.checked)} />
<span>
{' '}
<Link to="/legal/user-agreement" className="text-primary" style={{ fontWeight: 600 }} onClick={(e) => e.stopPropagation()}>
</Link>{' '}
{' '}
<Link to="/legal/privacy-policy" className="text-primary" style={{ fontWeight: 600 }} onClick={(e) => e.stopPropagation()}>
</Link>
</span>
</label>
);
}
function maskPhone(phone: string) {
if (phone.length < 7) return phone;
return `${phone.slice(0, 3)} **** ${phone.slice(-4)}`;
@@ -74,6 +100,7 @@ export default function LoginPage() {
const [msg, setMsg] = useState('');
const [codeCooldown, setCodeCooldown] = useState(0);
const [wxAuthorize, setWxAuthorize] = useState(false);
const agreementRef = useRef<HTMLLabelElement>(null);
useEffect(() => {
fetchClientConfig()
@@ -88,6 +115,7 @@ export default function LoginPage() {
function ensureAgreed() {
if (!agreed) {
setMsg('请先勾选并同意用户协议');
agreementRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return false;
}
return true;
@@ -230,10 +258,16 @@ export default function LoginPage() {
<nav style={{ width: '100%', maxWidth: 384, marginTop: 16 }}>
{msg && <p className="partner-auth-msg" style={{ color: 'var(--color-error, #d33)', fontSize: 13, textAlign: 'center', marginBottom: 12 }}>{msg}</p>}
<AgreementCheckbox
agreed={agreed}
onChange={setAgreed}
inputRef={agreementRef}
/>
{canWechatQuick ? (
<button
type="button"
className="partner-btn-primary"
style={{ marginTop: 16 }}
disabled={wxLoading}
onClick={() => void wechatLogin()}
>
@@ -241,7 +275,7 @@ export default function LoginPage() {
<span>{wxLoading ? '登录中...' : '微信一键登录'}</span>
</button>
) : (
<p className="partner-auth-msg" style={{ textAlign: 'center', marginBottom: 12 }}>
<p className="partner-auth-msg" style={{ textAlign: 'center', marginTop: 12, marginBottom: 12 }}>
{wxAuthorize && !isWechatEnv()
? '请在微信内打开以使用一键登录'
: '请使用验证码登录并绑定微信后,即可 7 天内免登录'}
@@ -321,6 +355,12 @@ export default function LoginPage() {
<span></span>
</label>
<AgreementCheckbox
agreed={agreed}
onChange={setAgreed}
inputRef={agreementRef}
/>
{msg && <p className="partner-auth-msg" style={{ color: 'var(--color-error, #d33)', fontSize: 13, textAlign: 'center' }}>{msg}</p>}
<button type="button" className="partner-btn-primary" onClick={() => void login()} disabled={loading}>
@@ -339,20 +379,6 @@ export default function LoginPage() {
</button>
</>
)}
<label className="partner-checkbox-row">
<input type="checkbox" checked={agreed} onChange={(e) => setAgreed(e.target.checked)} />
<span>
{' '}
<Link to="/legal/user-agreement" className="text-primary" style={{ fontWeight: 600 }} onClick={(e) => e.stopPropagation()}>
</Link>{' '}
{' '}
<Link to="/legal/privacy-policy" className="text-primary" style={{ fontWeight: 600 }} onClick={(e) => e.stopPropagation()}>
</Link>
</span>
</label>
</div>
</main>
+27 -4
View File
@@ -13,13 +13,23 @@ html {
/* ── Partner auth ── */
.partner-auth-page {
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px var(--space-page) var(--space-page);
justify-content: flex-start;
padding: 48px var(--space-page) calc(24px + env(safe-area-inset-bottom, 0px));
background-color: var(--color-background);
background-image: url("https://www.transparenttextures.com/patterns/natural-paper.png");
box-sizing: border-box;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
@media (min-height: 720px) {
.partner-auth-page {
justify-content: center;
}
}
.partner-auth-page--quick {
@@ -166,14 +176,27 @@ html {
.partner-checkbox-row {
display: flex;
align-items: center;
align-items: flex-start;
gap: 12px;
margin-top: var(--space-md);
font-size: 12px;
line-height: 1.5;
color: var(--color-on-surface-variant);
}
.partner-checkbox-row input { margin-top: 0; accent-color: var(--color-heritage-red); }
.partner-checkbox-row input {
margin-top: 2px;
flex-shrink: 0;
width: 18px;
height: 18px;
accent-color: var(--color-heritage-red);
}
.partner-checkbox-row--agreement {
margin-top: 8px;
margin-bottom: 4px;
padding: 8px 0;
}
.partner-auth-footer {
margin-top: var(--space-lg);
+44 -19
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState, type RefObject } from 'react';
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import { isWxAuthorizeEnabled } from '@dukang/shared-types';
@@ -28,6 +28,36 @@ function formatWechatError(e: unknown): string {
return text;
}
function ShopAgreementCheckbox({
agreed,
onChange,
labelRef,
}: {
agreed: boolean;
onChange: (next: boolean) => void;
labelRef?: RefObject<HTMLLabelElement | null>;
}) {
return (
<label className="shop-login-agreement" ref={labelRef}>
<input
type="checkbox"
checked={agreed}
onChange={(e) => onChange(e.target.checked)}
/>
<span>
<Link to="/legal/user-agreement" onClick={(e) => e.stopPropagation()}>
</Link>
<Link to="/legal/privacy-policy" onClick={(e) => e.stopPropagation()}>
</Link>
</span>
</label>
);
}
export default function LoginPage() {
const navigate = useNavigate();
const { applySession } = useStoreSession();
@@ -42,6 +72,7 @@ export default function LoginPage() {
const [msg, setMsg] = useState('');
const [codeCooldown, setCodeCooldown] = useState(0);
const [wxAuthorize, setWxAuthorize] = useState(false);
const agreementRef = useRef<HTMLLabelElement>(null);
useEffect(() => {
fetchClientConfig()
@@ -71,6 +102,7 @@ export default function LoginPage() {
function ensureAgreed() {
if (!agreed) {
setMsg('请先阅读并同意用户协议');
agreementRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return false;
}
return true;
@@ -180,6 +212,11 @@ export default function LoginPage() {
<div className="shop-quick-actions">
{msg && <p className="shop-login-msg">{msg}</p>}
<ShopAgreementCheckbox
agreed={agreed}
onChange={setAgreed}
labelRef={agreementRef}
/>
{canWechatQuick ? (
<button
type="button"
@@ -270,6 +307,12 @@ export default function LoginPage() {
{msg && <p className="shop-login-msg">{msg}</p>}
<ShopAgreementCheckbox
agreed={agreed}
onChange={setAgreed}
labelRef={agreementRef}
/>
<button
type="button"
className="shop-login-submit"
@@ -300,24 +343,6 @@ export default function LoginPage() {
</>
)}
</div>
<label className="shop-login-agreement">
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
<span>
<Link to="/legal/user-agreement" onClick={(e) => e.stopPropagation()}>
</Link>
<Link to="/legal/privacy-policy" onClick={(e) => e.stopPropagation()}>
</Link>
</span>
</label>
</main>
<footer className="shop-login-footer">
+18 -5
View File
@@ -188,7 +188,7 @@
.shop-login-submit {
width: 100%;
margin-top: 32px;
margin-top: 16px;
padding: 16px;
border: none;
border-radius: var(--radius-md);
@@ -248,14 +248,18 @@
.shop-login-agreement {
display: flex;
align-items: center;
align-items: flex-start;
gap: 8px;
max-width: 280px;
margin: 32px auto 0;
max-width: 100%;
margin: 16px 0 0;
padding: 8px 0;
}
.shop-login-agreement input {
margin-top: 0;
margin-top: 2px;
flex-shrink: 0;
width: 18px;
height: 18px;
accent-color: var(--color-heritage-red);
}
@@ -455,6 +459,15 @@
margin-top: auto;
}
.shop-quick-actions .shop-login-agreement {
margin: 0 0 16px;
max-width: 100%;
}
.shop-quick-actions .shop-quick-login-btn {
margin-top: 4px;
}
.shop-quick-login-btn {
width: 100%;
height: 56px;
+20 -21
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useSearchParams, Link } from 'react-router-dom';
import AppImage from '@dukang/shared-ui/AppImage';
import type { WechatLoginResult } from '@dukang/shared-types';
@@ -31,6 +31,7 @@ export default function LoginPage() {
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
const [bindMode, setBindMode] = useState(false);
const [wxAuthorize, setWxAuthorize] = useState(false);
const agreementRef = useRef<HTMLLabelElement>(null);
const { sendCode, sending, codeCooldown, sentHint, error: smsError, setError: setSmsError, clearMessages } =
useSmsCode();
@@ -73,6 +74,7 @@ export default function LoginPage() {
function ensureAgreed() {
if (!agreed) {
setMsg('请先勾选并同意用户协议');
agreementRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
return false;
}
return true;
@@ -194,6 +196,23 @@ export default function LoginPage() {
{displayMsg || sentHint}
</p>
)}
<label className="login-agreement" ref={agreementRef}>
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
<span>
<Link to="/legal/user-agreement" onClick={(e) => e.stopPropagation()}>
</Link>
<Link to="/legal/privacy-policy" onClick={(e) => e.stopPropagation()}>
</Link>
</span>
</label>
<button
type="button"
className="login-sms-btn"
@@ -219,26 +238,6 @@ export default function LoginPage() {
</>
)}
</main>
<footer className="login-footer">
<label className="login-agreement">
<input
type="checkbox"
checked={agreed}
onChange={(e) => setAgreed(e.target.checked)}
/>
<span>
<Link to="/legal/user-agreement" onClick={(e) => e.stopPropagation()}>
</Link>
<Link to="/legal/privacy-policy" onClick={(e) => e.stopPropagation()}>
</Link>
</span>
</label>
</footer>
</div>
);
}
+4 -3
View File
@@ -1558,8 +1558,9 @@
.login-agreement {
display: flex;
align-items: center;
align-items: flex-start;
gap: 8px;
margin: 12px 0 16px;
font-family: var(--font-label);
font-size: 12px;
font-weight: 500;
@@ -1571,8 +1572,8 @@
.login-agreement input {
margin-top: 0;
width: 16px;
height: 16px;
width: 18px;
height: 18px;
flex-shrink: 0;
accent-color: var(--color-heritage-red);
}
+5
View File
@@ -177,6 +177,11 @@
color: var(--hq-muted);
}
.login-agreement {
margin: 8px 0 12px;
padding: 4px 0;
}
.login-remember {
align-items: center;
padding: 0 4px;
+18 -18
View File
@@ -212,24 +212,6 @@ export default function LoginPage() {
<Text></Text>
</View>
<Button className="hq-btn hq-btn--primary hq-btn--block login-submit" loading={loading} onClick={smsLogin}>
</Button>
{msg ? <Text className="login-msg">{msg}</Text> : null}
<View className="login-divider">
<Text></Text>
</View>
<View
className={`login-wechat-btn${wxLoading ? ' is-disabled' : ''}`}
onClick={wxLoading ? undefined : wechatLogin}
>
<WechatIcon />
<Text>{wxLoading ? '登录中...' : '微信一键授权'}</Text>
</View>
<View className="login-agreement" onClick={() => setAgreed((v) => !v)}>
<View className={`login-checkbox${agreed ? ' is-checked' : ''}`} />
<Text className="login-agreement-text">
@@ -255,6 +237,24 @@ export default function LoginPage() {
</Text>
</Text>
</View>
{msg ? <Text className="login-msg">{msg}</Text> : null}
<Button className="hq-btn hq-btn--primary hq-btn--block login-submit" loading={loading} onClick={smsLogin}>
</Button>
<View className="login-divider">
<Text></Text>
</View>
<View
className={`login-wechat-btn${wxLoading ? ' is-disabled' : ''}`}
onClick={wxLoading ? undefined : wechatLogin}
>
<WechatIcon />
<Text>{wxLoading ? '登录中...' : '微信一键授权'}</Text>
</View>
</View>
<View className="login-footer">
+55 -30
View File
@@ -329,6 +329,33 @@ export default function LoginPage() {
<Text className="login-msg login-msg--hint" style={{ marginBottom: 16 }}>
使
</Text>
<View className="login-agreement" onClick={() => setAgreed((v) => !v)}>
<View className={`login-agreement-check${agreed ? ' login-agreement-check--on' : ''}`}>
{agreed ? <Text></Text> : null}
</View>
<Text className="login-agreement-text">
<Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/user-agreement/index' });
}}
>
</Text>
<Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/privacy-policy/index' });
}}
>
</Text>
</Text>
</View>
{showWechatLogin ? (
<WechatLoginButton loading={wxLoading} onClick={() => void wechatLogin()} />
) : null}
@@ -376,6 +403,34 @@ export default function LoginPage() {
</Text>
) : null}
<View className="login-agreement" onClick={() => setAgreed((v) => !v)}>
<View className={`login-agreement-check${agreed ? ' login-agreement-check--on' : ''}`}>
{agreed ? <Text></Text> : null}
</View>
<Text className="login-agreement-text">
<Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/user-agreement/index' });
}}
>
</Text>
<Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/privacy-policy/index' });
}}
>
</Text>
</Text>
</View>
<View
className={`login-sms-btn${loading ? ' login-sms-btn--disabled' : ''}`}
onClick={loading ? undefined : () => void login()}
@@ -415,36 +470,6 @@ export default function LoginPage() {
</>
) : null}
</View>
<View className="login-footer">
<View className="login-agreement" onClick={() => setAgreed((v) => !v)}>
<View className={`login-agreement-check${agreed ? ' login-agreement-check--on' : ''}`}>
{agreed ? <Text></Text> : null}
</View>
<Text className="login-agreement-text">
<Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/user-agreement/index' });
}}
>
</Text>
<Text
className="login-agreement-link"
onClick={(e) => {
e.stopPropagation();
Taro.navigateTo({ url: '/pages/privacy-policy/index' });
}}
>
</Text>
</Text>
</View>
</View>
</PageShell>
);
}
+2 -1
View File
@@ -306,8 +306,9 @@
.login-agreement {
display: flex;
align-items: center;
align-items: flex-start;
gap: 8px;
margin: 12px 0 16px;
}
.login-agreement-check {