本文记录把 react-native-otp-inputs(OTP / 验证码分格输入框)适配到 HarmonyOS 的完整过程。

它做的事很专一:把一串验证码(4 位、6 位…)拆成几个独立的输入格,输入一位自动跳到下一格、退格回到上一格,并可选地从剪贴板自动填充。

结论:可以正常使用,判据 19/19 全部通过。 但它有一个"藏在入口里"的原生前置,以及一个会在控制台刷警告的库设计问题 —— 这两点都比"能不能跑"更值得写下来。

★★★★ 一、"5 个文件里只有 1 个用剪贴板"是没用的:入口无条件 import

库的入口第一行就是:

// src/index.tsx L1
import Clipboard from '@react-native-clipboard/clipboard';

而它真正的用点只有两处:

L110: Clipboard.setString('');            // 在 ref.reset() 里
L205: const copiedText = await Clipboard.getString();   // 在剪贴板自动填充轮询里

看起来"不做自动填充就不需要剪贴板"——但打包层面不行:这个 import 在入口,Metro 必须解析整个模块图,不装 @react-native-clipboard/clipboard 就 Unable to resolve module。
而它是原生库:上游 @react-native-clipboard/clipboard@1.16.3,鸿蒙适配版是 @react-native-ohos/clipboard@1.17.0(存在 ✓),但接入它要接 HAR —— 而用同样方式接入原生库 HAR 时,曾出现过 BUILD FAILED(Error Code: 00308018,2.5 秒即失败,属于配置/解析期错误而非编译错误)。

★★★★ 我的做法:打一个带调用计数的空桩

// metro.config.js 的 resolver.extraNodeModules(★ 必须写在传给 mergeConfig 的对象里)
'@react-native-clipboard/clipboard': path.resolve(__dirname, 'stubs/react-native-clipboard.js'),
// stubs/react-native-clipboard.js —— 关键:计数 + 打日志
let getStringCalls = 0;
let setStringCalls = 0;
const Clipboard = {
  getString: () => { getStringCalls += 1;
    console.log(`[clipboard-stub] NOTE | getString 被调用(累计 ${getStringCalls})`); return Promise.resolve(''); },
  setString: () => { setStringCalls += 1;
    console.log(`[clipboard-stub] NOTE | setString 被调用(累计 ${setStringCalls})`); },
  …
  __stubStats: () => ({getStringCalls, setStringCalls}),
};

★ 为什么"计数"很重要:它把"桩被不被调用"变成了可断言的证据:

时刻getStringsetString说明
挂载后(默认路径)00库默认不碰剪贴板 ✓
调 ref.reset() 之后01桩确实接管了原生调用 ✓
NOTE | ★ clipboard 桩统计 = {"getStringCalls":0,"setStringCalls":0}
PASS | ★★★ 默认路径下 getString 调用 0 次(autofillFromClipboard=false)
(hilog 里 clipboard-stub 的日志:一条都没有)
点击「调 E1 的 reset()」之后:
NOTE | setString 被调用(累计 1)—— 只有 ref.reset() 会走到这里

⇒ 一个"两阶段"的行为判据:默认 0 → 主动调用后 1 ✓
⇒ 代价(如实记录):ref.reset() 与剪贴板自动填充实际上不工作(桩只返回空串)——见"已知限制"。

★★★★ 二、为什么"默认不碰剪贴板":一处平台判断的精确推算

// src/index.tsx L30-31
const supportAutofillFromClipboard =
  Platform.OS === 'android' || parseInt(Platform.Version as string, 10) < 14;
…
L61: autofillFromClipboard = supportAutofillFromClipboard,   // 默认值
…
L221: if (autofillFromClipboard) { interval = setInterval(() => listenOnCopiedText(), …); }

在鸿蒙上逐项推算:

Platform.OS === 'android'            ⇒ false
Platform.Version                     ⇒ "OpenHarmony-7.0.0.32(Beta2)"
parseInt("OpenHarmony-7.0.0.32(Beta2)", 10) ⇒ NaN
NaN < 14                             ⇒ false
⇒ supportAutofillFromClipboard = false   ★ 默认不启用剪贴板轮询

实测(页面上复算 + 桩计数双重证实):

NOTE | Platform.OS = harmony
NOTE | Platform.Version = OpenHarmony-7.0.0.32(Beta2)
NOTE | ★ 复算 supportAutofillFromClipboard = false
PASS | ★★★ 鸿蒙上 supportAutofillFromClipboard 应为 false(⇒ 默认不轮询剪贴板) | 期望=false | 实际=false
PASS | ★★★ 默认路径下 getString 调用 0 次(autofillFromClipboard=false)   | 期望=0 | 实际=0

⇒ "NaN < 14 为假"这个链条被完整验证 ✓ —— 这也说明:凡是 parseInt(Platform.Version) 参与的平台判断,在鸿蒙上都会得到 NaN,写这类判断时要留意。

顺带看完整库的 27 处平台判断:Platform.OS 18 处、Platform.select 9 处。
逐处看过之后,只有 2 处会得到 undefined(且都不崩):

位置代码鸿蒙上的结果
src/index.tsx L264maxLength={Platform.select({android: 1, ios: …})}undefined ⇒ 不崩,但失去"整串粘贴进第一格"的能力
src/OtpInput.tsx L78selectTextOnFocus={Platform.select({ios: …, android: true})}undefined ⇒ 用 TextInput 默认值
src/OtpInput.tsx L57Platform.select({default: rest, web: {…}})✅ 带了 default ⇒ 返回 rest(正面写法)
src/index.tsx L149/L164Platform.OS === 'android' && … / === 'ios' && …全 false ⇒ 分支不进入 ✓
src/OtpInput.tsx L26Platform.OS === 'ios' && majorVersionIOS >= 12false ⇒ textContentType='none' ✓

⇒ 一个"第四平台"(既非 iOS 也非 Android 也非 Web)的典型走法:二元判断天然落到 else,安全;没有 default 的 Platform.select 会得到 undefined,只要接收方容得下 undefined 就不崩 ✓

★★★★★ 三、13 条纯函数判据:全部精确命中

交付包同时给了 src/(TS 源码)和 lib/(编译产物),而 lib/commonjs/ 里的两个纯函数可以直接用子路径 require 单测:

const {fillOtpCode} = require('react-native-otp-inputs/lib/commonjs/helpers');
const reducer = require('react-native-otp-inputs/lib/commonjs/reducer').default;

3.1 fillOtpCode(numberOfInputs, value)

const fillOtpCode = (numberOfInputs, value) => {
  const otpCode = {};
  for (let i = 0; i < numberOfInputs; i++) {
    otpCode[`${i}`] = value?.[i] || '';       // ★ 不足位补空串
  }
  return otpCode;
};
调用预期实测
fillOtpCode(4){"0":"","1":"","2":"","3":""}✅
fillOtpCode(4, '1234'){"0":"1","1":"2","2":"3","3":"4"}✅
fillOtpCode(4, '12'){"0":"1","1":"2","2":"","3":""}✅ ★ 覆盖"不足位补空串"
fillOtpCode(0, '1234'){}✅ ★ 覆盖 numberOfInputs=0

3.2 reducer(state, {type, payload})

它有 5 个 action,且每个 action 内部都会调用一次 state.handleChange(...):

case ACTION_TYPES.setOtpTextForIndex: {
  const otpCode = {...state.otpCode, [`${payload.index}`]: payload.text};
  state.handleChange(Object.values(otpCode).join(''));      // ★ 副作用
  return {...state, otpCode};
}
…
default: return state;                                       // ★ 返回同一引用

实测(9 条断言全部通过):

PASS | ★★ setOtpTextForIndex 写入指定下标            | {"0":"","1":"7","2":"","3":""}
PASS | ★★ 且回调参数是 Object.values().join("")       | "7"
PASS | ★  返回新对象(不是同一引用)
PASS | ★★ setOtpCode 走 fillOtpCode                  | {"0":"9","1":"8","2":"7","3":"6"}
PASS | ★★ setOtpCode 回调                            | "9876"
PASS | ★★ clearOtp 清空(fillOtpCode(payload))      | {"0":"","1":"","2":"","3":""}
PASS | ★★ clearOtp 回调为空串                        | ""
PASS | ★★ setHandleChange 替换回调(同一引用)
PASS | ★★ setHasKeySupport 置位
PASS | ★★ 未知 action 返回【同一引用】                ← ★ 用 === 断言

⇒ 纯函数是最便宜的硬判据:不需要渲染、不需要设备交互,而且能覆盖边界(numberOfInputs=0、不足位、未知 action)✓

★★★★ 四、accessibilityLabel 让它"天然可数",还顺手验证了 isRTL

组件给每个输入框设了可预测的 accessibilityLabel:

accessibilityLabel={`${testIDPrefix}-${inputIndex}`}      // testIDPrefix 默认 "otpInput"

⇒ 布局 dump 里直接读得到,于是"有几个输入框、每个多大、什么顺序"全都可验证 ✓

实测(E1:4 格,每格 inputStyles 60vp):

E1(60vp ⇒ 180px)去重后节点数 = 4    ← 恰好 4 个 ✓
TextInput#otpInput-0 [66,1110,246,1290]      ⇒ 180×180 ✓ = 60vp×3 ✓
TextInput#otpInput-1 [402,1110,582,1290]
TextInput#otpInput-2 [738,1110,918,1290]
TextInput#otpInput-3 [1074,1110,1254,1290]

实测(E2:6 格 + isRTL={true},每格 44vp):

E2(44vp ⇒ 132px)去重后节点数 = 6    ← 恰好 6 个 ✓
TextInput#otpInput-5 [66,1459,198,1591]        ⇒ 132×132 ✓ = 44vp×3 ✓
TextInput#otpInput-4 [277,…]
TextInput#otpInput-3 [488,…]
TextInput#otpInput-2 [700,…]
TextInput#otpInput-1 [911,…]
TextInput#otpInput-0 [1122,…]

⇒ ★★★ 注意 E2 的标签从左到右是 5,4,3,2,1,0 —— 这正是源码里:

let inputIndex = index;
if (isRTL) { inputIndex = numberOfInputs - 1 - index; }      // src/index.tsx L237-239

的效果 ✓✓ isRTL 被直接证实(一个判据同时验证了"格数"、"尺寸"和"RTL 反转"三件事)✓

★★★★ 五、库的一个真实设计问题:reducer 在渲染期调了回调

页面上出现了这样一条 LogBox 通知(不是崩溃,是警告):

"Cannot update a component (`OtpInputsTestApp`) while rendering a different component (`ForwardRef`).
 To locate the bad setState() call inside `ForwardRef`, follow the sta…"

根因在源码里一目了然:reducer 的每个 action 分支都调用 state.handleChange(...),
而 useReducer 的 reducer 是在渲染阶段执行的 ⇒ handleChange 在渲染期被调用 ⇒
如果调用方在 handleChange 里做 setState(我页面上就是这么写的),React 就会报"不能在渲染另一个组件时更新组件"。

// src/index.tsx L67:默认值是 console.log
handleChange = console.log,

⇒ ★ 一个实用的规避建议:如果不需要在输入变化时立刻 setState,就别传 handleChange(默认的 console.log 不会有这个问题);
或者把 setState 包进 setTimeout(…, 0) / queueMicrotask 之类延后一拍。

这一条和第三节的 A3 判据是互相印证的:判据证明"每个 action 都会调 handleChange",
而运行时警告证明"这个调用发生在渲染期" —— 源码事实和运行时行为对上了 ✓

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


六、接入与预检

步做法结果
①harmony.autolinking / harmony/ / ios/ / android/都没有
②原生痕迹预扫✅ 无原生源文件(NativeModules/requireNativeComponent/TurboModule 全空)
③★ 老写法九项预扫✅✅ 全部为空(见下)
④measure() / onLayout / useNativeDriver / findNodeHandle✅ 全空(没有那类环境坑)
⑤平台分支⚠️ Platform.OS 18 处 + Platform.select 9 处 ⇒ 逐处看过,只有 2 处得 undefined、都不崩(见第二节)
⑥依赖✅ dependencies 为空;⚠️ peer 里有 @react-native-clipboard/clipboard,且入口无条件 import ⇒ 硬前置
⑦scripts 扫 prepare 家族⚠️ 有("prepare": "bob build")⇒ 必须 --ignore-scripts

老写法九项逐项结果:

项结果
React.createClass✅ 空
React.PropTypes✅ 空
从 react 具名导入 PropTypes✅ 空
ViewPropTypes / TextPropTypes✅ 空
字符串 ref="x" / this.refs.x✅ 空
mixins:✅ 空
componentWillReceiveProps✅ 空
componentWillMount✅ 空
measure( / measureInWindow✅ 空

它还用了很现代的写法:forwardRef + useImperativeHandle + useReducer + React.memo ✓

# 从适配仓库安装(零 dependencies)
npm install "git+https://atomgit.com/oh-react-native/react-native-otp-inputs.git#7.4.0-ohos-1.0.0" --ignore-scripts
# ⚠️ 还必须解决 @react-native-clipboard/clipboard(入口无条件 import 的原生前置):
#    正式做法:npm install @react-native-ohos/clipboard 并接入其 HAR
#    隔离验证:在 metro.config.js 的 resolver.extraNodeModules 里把它指向空桩

七、交付包质量与基线说明

类别结果
交付包内容src/(TS 源码)+ lib/(commonjs/module/typescript)都在 ✓ 源码可读 ✓
spec.json⚠️ 缺 upstreamCommit
validation⚠️ "build-pass-device-pending"
⚠️ 声明的基线reactNative: "0.88.0" + rnoh: "0.84.4"(verification: evidence/batch50/…)⇒ 两字段自相矛盾(rnoh 0.84.4 是配 RN 0.84.x 的),且既不是当前宿主(0.84.1/0.84.3)也不是权威 0.88 基线

关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。这个库没有任何已移除的写法,
行为只依赖 forwardRef / useReducer / TextInput / Platform.select ⇒ 结论跨基线成立 ✓
(唯一需要复验的是第二节那条 parseInt(Platform.Version) 的推算 —— 它取决于 Platform.Version 的字符串形态。)


八、已知限制与本次验证边界

8.1 用这个库要知道的

事项说明
★★★ @react-native-clipboard/clipboard 是硬前置入口无条件 import ⇒ 不装就 Unable to resolve module;它是原生库(鸿蒙适配版为 @react-native-ohos/clipboard)
★★★ reducer 在渲染期调 handleChange若你在 handleChange 里 setState,会出现 Cannot update a component while rendering a different component 警告 ⇒ 不必要时别传 handleChange(默认 console.log)
★★ 鸿蒙上 supportAutofillFromClipboard 恒为 false因为 parseInt("OpenHarmony-…") 是 NaN ⇒ 剪贴板自动填充默认不启用
★★ maxLength 在鸿蒙上是 undefinedPlatform.select({android:1, ios:…}) 没有 default ⇒ 失去"整串粘贴进第一格"的能力(可自行传 maxLength 覆盖)
★ accessibilityLabel 可预测otpInput-0、otpInput-1…(前缀可用 testIDPrefix 改)⇒ 自动化测试很好用
★ isRTL 会把标签顺序反转inputIndex = numberOfInputs - 1 - index(实测标签从左到右是 5,4,3,2,1,0)
★ 每格尺寸由你的 inputStyles 决定实测:width/height: 60 ⇒ 180px;44 ⇒ 132px(density 3)
★ ref 暴露 reset() 与 focus()可编程调用(reset() 会走一次 Clipboard.setString(''))

8.2 本次验证的边界

  1. ★★ 剪贴板功能未真正验证(用空桩隔离;ref.reset() 实际不清空剪贴板、自动填充也不工作);
  2. ★ 未做真机键盘输入验证(本轮全部用 ref 编程调用与布局 dump,没有用 uitest inputText 输入 —— 该手段曾对 RNOH 输入框无效);
  3. 未验证逐位输入时的自动跳格/退格回退行为(需要真实键盘事件);
  4. 未验证 autofillFromClipboard={true} 时轮询能找到剪贴板里的验证码;
  5. 未验证 secureTextEntry / placeholder / autoFocus / clearTextOnFocus / focusStyles 的实际效果;
  6. 未验证 keyboardType 各取值;
  7. 未验证 maxLength 为 undefined 时的实际输入行为(只从源码推断"失去整串粘贴");
  8. 未验证 onKeyPress 相关分支(hasKeySupport 在鸿蒙上恒 false);
  9. 未在 RN 0.88.0 / RNOH 0.84.4 上复验(交付包声明的基线);
  10. 未测其它屏幕宽度的布局(只验证了 440vp);
  11. 未做跨平台对照。

未改动库代码。


九、常见问题

Q1:它在 RN 0.84 上能跑吗?

能。 本次 19/19 条判据全部通过:契约、两个纯函数(13 条精确断言)、平台推算、渲染几何、ref 编程调用都正确。

Q2:我只用 OtpInputs,一定要装 clipboard 吗?

要。 入口 src/index.tsx 第一行就 import Clipboard from '@react-native-clipboard/clipboard' ⇒ Metro 必须解析到它。
两条路:装鸿蒙适配版 @react-native-ohos/clipboard 并接 HAR;或在 metro.config.js 的 resolver.extraNodeModules 里指向空桩(本轮做法,代价是剪贴板功能不可用)。

Q3:为什么我传了 handleChange 就报警告?

因为库的 reducer 在每个 action 内部都调用 state.handleChange(...),而 useReducer 的 reducer 在渲染期执行 ⇒
渲染期触发了你的 setState ⇒ React 报 Cannot update a component while rendering a different component。
不必要时别传 handleChange(默认是 console.log),或把 setState 延后一拍。

Q4:剪贴板自动填充为什么没生效?

看这段:

const supportAutofillFromClipboard = Platform.OS === 'android' || parseInt(Platform.Version, 10) < 14;

鸿蒙上 Platform.OS 不是 android,而 parseInt("OpenHarmony-7.0.0.32(Beta2)") 是 NaN ⇒ NaN < 14 为 false ⇒ 默认不启用。
需要的话显式传 autofillFromClipboard={true}。

Q5:为什么粘一整串验证码进不去?

maxLength 是 Platform.select({android: 1, ios: …}),没有 default ⇒ 鸿蒙上得到 undefined。
显式传 maxLength 即可绕过(或自己实现粘贴处理)。

Q6:怎么在不点屏幕、不敲键盘的情况下测试它?

用 ref:

const ref = useRef(null);
<OtpInputs ref={ref} numberOfInputs={4} handleChange={c => {}} />

ref.current.focus();    // 聚焦第一格
ref.current.reset();    // 清空(会调一次 Clipboard.setString(''))

Q7:怎么在自动化里核对"有几个格子"?

用 accessibilityLabel(otpInput-0、otpInput-1…),布局 dump 里能直接读到;
也可以按你的 inputStyles 算像素尺寸(density 3:60 ⇒ 180px)。
⚠️ isRTL 会让标签顺序反转(index = n-1-i),核对位置时注意。


小结

react-native-otp-inputs 是零 dependencies、老写法九项全空的现代函数组件库(forwardRef + useReducer + useImperativeHandle),
设备侧 19/19 条判据全部通过,主体可以正常使用。

最值得记下来的是这几条:

  1. ★★★★ "只有个别文件用到的原生库"在打包层面依然是硬前置。
    Clipboard 只在 ref.reset() 与自动填充里用,但 import 写在入口第一行 ⇒ 不装就 Unable to resolve module ✓
    ⇒ 判断"要不要装"不能看用点数量,要看 import 在不在入口的模块图里。

  2. ★★★★ 用"带调用计数的空桩"把"依赖能不能绕开"变成可断言的证据。
    getString/setString 计数 + 每次调用打日志 ⇒
    默认路径 0 次 / 调 reset() 后 setString 变 1 次 ✓ —— 两阶段行为判据,比"我觉得不影响"强得多 ✓

  3. ★★★★ 一处平台判断的完整推算 + 双重证实。
    Platform.OS === 'android' || parseInt(Platform.Version, 10) < 14
    在鸿蒙上 = false || (NaN < 14 = false) = false ⇒ 默认不轮询剪贴板 ✓
    页面上复算它、又用桩计数证实"确实没有被调用" ✓
    ⇒ 凡 parseInt(Platform.Version) 参与的平台判断,在鸿蒙上都会得到 NaN —— 这是一个通式。

  4. ★★★★ accessibilityLabel 让"格数/尺寸/顺序"三件事同时可验证。
    实测 E1 = 4 格 × 180px(60vp×3)、E2 = 6 格 × 132px(44vp×3),
    而且 E2 的标签从左到右是 5,4,3,2,1,0 ⇒ 直接把 isRTL 的 n-1-index 证实了 ✓

  5. ★★★★ 纯函数是最便宜的硬判据(13 条全部精确命中)。
    lib/commonjs/helpers 与 lib/commonjs/reducer 都能子路径 require ⇒
    fillOtpCode 4 条(含 numberOfInputs=0、不足位补空串)、reducer 9 条(5 个 action + 未知 action 返回同一引用)。
    ⚠️ 并发现 reducer 的每个 action 都会调 state.handleChange(...) —— 这条源码事实后来被运行时警告印证了 ✓

  6. ★★★ reducer 在渲染期调回调 ⇒ React 警告。
    Cannot update a component (…) while rendering a different component (…)
    ⇒ 规避:不必要时别传 handleChange(默认 console.log),或延后一拍再 setState。

  7. ★★ 27 处平台判断里只有 2 处得 undefined、且都不崩。
    无 default 的 Platform.select 返回 undefined(maxLength、selectTextOnFocus);
    而带 default 的那一处是正面写法(Platform.select({default: rest, web: …}))✓

  8. ★ 原生库 HAR 的接入不是"照抄既有一行就行"。
    用同样方式接入原生库 HAR 时曾遇到 BUILD FAILED(Error Code: 00308018,2.5 秒即失败 ⇒ 配置/解析期错误);
    ⇒ 所以本轮选择打桩隔离这条零原生接线的路径,把结论做扎实、并把"未验证项"如实列出 ✓


本篇用到的库

项内容
三方库react-native-otp-inputs(上游 7.4.0 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-otp-inputs
适配 TAG7.4.0-ohos-1.0.0
需要 HAR / 权限 / ohpm⚠️ 库本身不需要;但它硬依赖 @react-native-clipboard/clipboard(原生库)⇒ 该库需接 HAR(或打桩隔离)
库类型OTP / 验证码分格输入框
运行时依赖✅ 无 dependencies;⚠️ peer 里的 @react-native-clipboard/clipboard 被入口无条件 import
prepare / postinstall⚠️ 有("prepare": "bob build")⇒ 安装加 --ignore-scripts
许可证MIT
上游仓库https://github.com/dsznajder/react-native-otp-inputs(★248)
宿主工程RNOH084Demo(测试页 rnAppKey = OtpInputsTestApp)
# 从适配仓库安装
npm install "git+https://atomgit.com/oh-react-native/react-native-otp-inputs.git#7.4.0-ohos-1.0.0" --ignore-scripts
# ⚠️ 还必须解决 @react-native-clipboard/clipboard(原生前置):
#    npm install @react-native-ohos/clipboard   + 接入其 harmony 下的 HAR
#    或:metro.config.js 的 resolver.extraNodeModules 指向空桩(见正文第一节)
import OtpInputs from 'react-native-otp-inputs';

const ref = useRef(null);

<OtpInputs
  ref={ref}
  numberOfInputs={4}                       // ⇒ 4 个输入格(accessibilityLabel: otpInput-0..3)
  handleChange={c => { /* ⚠️ 别在这里 setState —— reducer 在渲染期调用它 */ }}
  inputStyles={{width: 60, height: 60}}    // ⇒ 60vp × 3 = 180px
  inputContainerStyles={{marginHorizontal: 2}}
  keyboardType="phone-pad"
  // maxLength={1}                         // ⚠️ 自己传,否则鸿蒙上是 undefined
  // autofillFromClipboard={false}         // 鸿蒙上默认就是 false
  // isRTL={false}                         // 打开会把 accessibilityLabel 顺序反转
/>

// 编程控制
// ref.current.focus();   // 聚焦第一格
// ref.current.reset();   // 清空(会调一次 Clipboard.setString(''))

// ⚠️ 入口 src/index.tsx L1 无条件 import '@react-native-clipboard/clipboard'(原生库)
// ⚠️ 不必要时别传 handleChange(默认是 console.log)⇒ 可避免渲染期 setState 警告
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey OtpInputsTestApp

判据的复现方式

# ① 契约 / 两个纯函数 / 平台推算:逐条 PASS/FAIL 日志
hdc shell "hilog -x | grep otp-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
#    ⇒ fillOtpCode 4 条 + reducer 9 条 + supportAutofillFromClipboard=false
# ② 空桩的行为判据(两阶段)
hdc shell "hilog -x | grep clipboard-stub"
#    ⇒ 默认:【一条日志都没有】;调 ref.reset() 后 ⇒ setString 累计 1
# ③ 几何 + 格数(accessibilityLabel 可直接读)
devecocli ui layout | Select-String 'otpInput-\d'
#    ⇒ E1: otpInput-0..3 各 180×180;E2: otpInput-5..0 各 132×132(isRTL 反转)

验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.js / npmv24.14.0 / 11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 26(26.0.0.32)
设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64),1320×2232,density 3
Platform.OS / Platform.Version(实测值)harmony / OpenHarmony-7.0.0.32(Beta2)
本机状态栏高度117px = 39vp
宿主依赖⚠️ @react-native-clipboard/clipboard 用带调用计数的空桩隔离
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap 3 分 44 秒 ⇒ 一次成功(打包与构建退出码都检查了)
验证规模19 条判据全部通过;13 条纯函数精确断言(fillOtpCode 4 + reducer 9);几何 2 组精确命中(4 格×180px / 6 格×132px);isRTL 标签反转证实;空桩两阶段计数(0 → 1);4 张截图互异

⚠️ 基线说明:交付包 spec.json 声明 RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
本库无任何已移除写法 ⇒ 契约与纯函数结论跨基线成立;但第二节那条 parseInt(Platform.Version) 的推算建议换基线时复验(它取决于 Platform.Version 的字符串形态)。


相关社区与项目地址

项目地址
React Native 鸿蒙官方社区(CPF-RN)https://atomgit.com/CPF-RN
RNOH 框架仓库https://atomgit.com/openharmony-RN/ohos_react_native
RNOH 三方库使用文档https://atomgit.com/OpenHarmony-RN/usage-docs
React Native for OpenHarmony 组织https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN

React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native

RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐