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