diff --git a/.well-known/assetlinks.json b/.well-known/assetlinks.json deleted file mode 100644 index 7de3d99d..00000000 --- a/.well-known/assetlinks.json +++ /dev/null @@ -1,26 +0,0 @@ -[{ - "relation": ["delegate_permission/common.handle_all_urls"], - "target": { - "namespace": "android_app", - "package_name": "co.tinode.tindroidx", - "sha256_cert_fingerprints": - ["25:9A:71:EE:BD:2E:9F:D3:2D:64:1E:90:49:33:BD:F3:E3:42:BD:C4:DD:FA:BA:68:EE:D2:EB:76:65:33:DC:92"] - } -}, -{ - "relation": ["delegate_permission/common.get_login_creds"], - "target": { - "namespace": "web", - "site": "https://web.tinode.co" - } -}, -{ - "relation": ["delegate_permission/common.get_login_creds"], - "target": { - "namespace": "android_app", - "package_name": "co.tinode.tindroidx", - "sha256_cert_fingerprints": - ["5B:BE:6A:C4:A7:66:A8:BB:4B:C1:52:66:E5:A4:BD:66:3C:33:50:04:11:CB:17:44:45:89:5E:3C:8D:55:77:7A"] - } -} -] diff --git a/css/base.css b/css/base.css index fd568ad2..3ff84fab 100644 --- a/css/base.css +++ b/css/base.css @@ -1099,6 +1099,13 @@ font-style: normal; } +/* DailyReportCard renders
, not the diff --git a/src/views/personal-settings-view.jsx b/src/views/personal-settings-view.jsx index a7988fbe..f0784315 100644 --- a/src/views/personal-settings-view.jsx +++ b/src/views/personal-settings-view.jsx @@ -705,9 +705,11 @@ function YouthModeVerify({hashParams, onToast}) { return
- - setPassword(e.target.value.replace(/\D/g, '').slice(0, 4))} aria-label="独立密码" /> +
+ + setPassword(e.target.value.replace(/\D/g, '').slice(0, 4))} aria-label="独立密码" /> +
{error ?
{error}
: null} \n

{title}

\n \n
;\n}\n\nfunction Field({icon, onIconClick, action, error, className = '', ...props}) {\n return
\n
\n {icon ? (onIconClick ?\n :\n {icon}) : null}\n \n {action}\n
\n {error ?
{error}
: null}\n
;\n}\n\nfunction PasswordField(props) {\n const [visible, setVisible] = React.useState(false);\n return setVisible(!visible)}\n aria-label={visible ? '隐藏密码' : '显示密码'}>{visible ? 'visibility' : 'visibility_off'}} />;\n}\n\nfunction Agreement({checked, onChange, prefix = '同意捷信密使'}) {\n return ;\n}\n\nfunction Toast({message, onDone}) {\n React.useEffect(() => {\n if (!message) return undefined;\n const timer = window.setTimeout(onDone, 2800);\n return () => window.clearTimeout(timer);\n }, [message, onDone]);\n return message ?
check_circle{message}
: null;\n}\n\n// Shared alert for errors coming back from a real API/WS call (as opposed to local form validation,\n// which each form already reports inline per-field).\nfunction FormAlert({text}) {\n return text ?
error_outline{text}
: null;\n}\n\nfunction TenantEntry({onTenantResolved}) {\n const [code, setCode] = React.useState(sessionStorage.getItem('im-tenant-code') || '');\n const [error, setError] = React.useState('');\n const [busy, setBusy] = React.useState(false);\n\n const submit = async e => {\n e.preventDefault();\n const value = code.trim();\n if (!value || value.length > 64) {\n setError(value ? '企业码不能超过 64 个字符' : '请输入企业码');\n return;\n }\n setBusy(true);\n setError('');\n try {\n const data = await resolveTenant(value);\n // Kept for the mock UI's own display of the tenant name while on this screen; the durable\n // context (read by every subsequent /v1/im/* call and by the {hi} tenant field) is persisted\n // by onTenantResolved.\n sessionStorage.setItem('im-tenant-code', data.tenant.code || value);\n sessionStorage.setItem('im-tenant-name', data.tenant.name || data.tenant.code || value);\n onTenantResolved(data, value);\n navigate('login');\n } catch (err) {\n // Per the product spec, tenant-resolve failures are shown as one generic message\n // regardless of reason (not found vs. disabled vs. rate limited).\n setError('企业码不存在或已停用,请核实后重试');\n } finally {\n setBusy(false);\n }\n };\n\n return
\n \n
\n \n { setCode(e.target.value); setError(''); }}\n placeholder=\"请输入企业码\" autoComplete=\"organization\" maxLength=\"64\" error={error} autoFocus />\n \n \n \n
;\n}\n\nfunction Login({tenantName, login, loginDisabled, persist, errorText, errorLevel, onLoginRequest, onPersistenceChange, onError}) {\n const [account, setAccount] = React.useState(login || '');\n const [password, setPassword] = React.useState('');\n const [agreed, setAgreed] = React.useState(false);\n\n // The prototype (docs/images/登录页面.png) has no \"stay logged in\" checkbox -- a consumer IM app\n // just keeps you signed in. Opt in silently instead of exposing the old debug-era toggle.\n React.useEffect(() => {\n if (!persist) {\n onPersistenceChange(true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Real login failures (wrong password, disabled account, ...) clear the password and keep the account,\n // matching the product spec.\n React.useEffect(() => {\n if (errorLevel === 'err' && errorText) {\n setPassword('');\n }\n }, [errorText, errorLevel]);\n\n const submit = e => {\n e.preventDefault();\n if (!account.trim() || !password || !agreed || loginDisabled) return;\n onLoginRequest(account.trim(), password);\n };\n\n return
\n \n
\n \n \n setAccount(e.target.value)}\n placeholder=\"请输入用户名/手机号\" autoComplete=\"username\" />\n setPassword(e.target.value)}\n placeholder=\"请输入密码\" autoComplete=\"current-password\" />\n
\n \n

没有账户?

\n \n \n
;\n}\n\nfunction EnterpriseRegister() {\n const [creditCode, setCreditCode] = React.useState('');\n const [email, setEmail] = React.useState('');\n const [file, setFile] = React.useState(null);\n const [errors, setErrors] = React.useState({});\n const [submitting, setSubmitting] = React.useState(false);\n const [submitted, setSubmitted] = React.useState(false);\n const [reviewDays, setReviewDays] = React.useState(1);\n const [toast, setToast] = React.useState('');\n\n React.useEffect(() => () => file && URL.revokeObjectURL(file.url), [file]);\n\n const chooseFile = e => {\n const selected = e.target.files[0];\n if (!selected) return;\n if (selected.size > 10 * 1024 * 1024 || !['image/jpeg', 'image/png', 'image/webp', 'application/pdf'].includes(selected.type)) {\n setErrors({...errors, file: '支持 JPG、PNG、WebP、PDF,文件不超过 10MB'});\n return;\n }\n if (file) URL.revokeObjectURL(file.url);\n setFile({name: selected.name, type: selected.type, url: URL.createObjectURL(selected), blob: selected});\n setErrors({...errors, file: ''});\n };\n\n const submit = async e => {\n e.preventDefault();\n const next = {};\n if (!CREDIT_CODE_RE.test(creditCode)) next.creditCode = '请输入 18 位统一社会信用代码';\n if (email && !EMAIL_RE.test(email)) next.email = '请输入有效的企业邮箱';\n setErrors(next);\n if (Object.keys(next).length) return;\n\n setSubmitting(true);\n try {\n let licenseFileId = '';\n if (file) {\n const uploaded = await uploadPublicFile(file.blob, 'enterprise_license');\n licenseFileId = uploaded.file_id;\n }\n const data = await submitTenantApplication({\n credit_code: creditCode,\n enterprise_email: email,\n license_file_id: licenseFileId,\n tenant_code_hint: sessionStorage.getItem('im-tenant-code') || ''\n });\n // Prototype (docs/images/企业注册-填写注册信息.png) states this in business days, not hours.\n setReviewDays(Math.max(1, Math.round((data.estimated_review_hours || 24) / 24)));\n setSubmitted(true);\n setToast('提交成功,我们会尽快联系你');\n } catch (err) {\n setErrors({...next, form: err.message || '提交失败,请稍后重试'});\n } finally {\n setSubmitting(false);\n }\n };\n\n return
\n
\n
\n {/* .active marks a step (and the connector after it) as reached, not \"this is the current\n step\" -- steps stay lit once passed. There's no separate server-tracked \"确定信息\" state\n (submitting the form *is* confirming it), so step 2 only ever gets skipped straight from\n 1 to 3; without submitted also keeping step 1 lit, submitting made it look like step 1 had\n been undone instead of completed. */}\n
    \n
  1. 1填写企业信息
  2. \n
  3. 2确定信息
  4. \n
  5. 3提交审核
  6. \n
\n
\n \n \n setCreditCode(e.target.value.trim().toUpperCase().replace(/[^0-9A-Z]/g, ''))}\n placeholder=\"请输入18位统一社会信用代码\" maxLength=\"18\" icon=\"qr_code_scanner\" error={errors.creditCode}\n onIconClick={() => setToast('扫码识别暂不可用,请手动输入')} />\n

请填写营业执照上的统一社会信用代码

\n\n \n \n {file ? : null}\n {errors.file ?
{errors.file}
: null}\n\n \n setEmail(e.target.value)}\n placeholder=\"请输入企业邮箱\" type=\"email\" error={errors.email} />\n

用于接收审核结果及企业通知

\n \n

schedule资料提交后,预计 {reviewDays} 个工作日完成审核

\n \n
\n setToast('')} />\n
;\n}\n\n// Per docs/images/企业注册-问题反馈.png: titled \"意见反馈\", 300-char textarea with the instructions\n// as its placeholder (not a separate label), \"图片(N/9)\" counter-style label, and no contact field.\nfunction EnterpriseFeedback() {\n const [description, setDescription] = React.useState('');\n const [files, setFiles] = React.useState([]);\n const [error, setError] = React.useState('');\n const [submitting, setSubmitting] = React.useState(false);\n const [toast, setToast] = React.useState('');\n\n const addFiles = e => {\n const selected = Array.from(e.target.files || []).slice(0, 9 - files.length);\n const valid = selected.filter(file => file.size <= 10 * 1024 * 1024 && ['image/jpeg', 'image/png', 'image/webp'].includes(file.type));\n setFiles(files.concat(valid.map(file => ({name: file.name, url: URL.createObjectURL(file), blob: file}))));\n };\n const submit = async e => {\n e.preventDefault();\n const length = description.trim().length;\n if (length < 10 || length > 300) {\n setError('问题描述需为 10 至 300 个字符');\n return;\n }\n setSubmitting(true);\n try {\n const screenshotFileIds = [];\n for (const item of files) {\n const uploaded = await uploadPublicFile(item.blob, 'tenant_entry_feedback');\n screenshotFileIds.push(uploaded.file_id);\n }\n await submitTenantEntryFeedback({\n description: description.trim(),\n tenant_code_hint: sessionStorage.getItem('im-tenant-code') || '',\n screenshot_file_ids: screenshotFileIds\n });\n setToast('已收到反馈,我们会尽快处理');\n files.forEach(item => URL.revokeObjectURL(item.url));\n setDescription(''); setFiles([]); setError('');\n } catch (err) {\n setError(err.message || '提交失败,请稍后重试');\n } finally {\n setSubmitting(false);\n }\n };\n\n return
\n
\n
\n
\n \n