Skip to content

Commit 728a56a

Browse files
authored
feat: add setUserInterfaceStyle API with local persistence (#16)
1 parent 03b15a3 commit 728a56a

16 files changed

Lines changed: 1249 additions & 31 deletions

File tree

.claude/skills/1k-code-review-pr/SKILL.md

Lines changed: 1076 additions & 0 deletions
Large diffs are not rendered by default.

.github/workflows/package-publish.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ jobs:
66
package-publish:
77
runs-on: ubuntu-latest
88
steps:
9-
- uses: actions/checkout@v2
10-
- uses: actions/setup-node@v2
9+
- uses: actions/checkout@v4
10+
- uses: actions/setup-node@v6
1111
with:
12-
node-version: '20.x'
12+
node-version: '24.x'
1313
registry-url: 'https://registry.npmjs.org'
1414
- name: Install Package
1515
run: corepack enable && yarn install

example/react-native/ios/Podfile.lock

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PODS:
2-
- BackgroundThread (1.1.15):
2+
- BackgroundThread (1.1.17):
33
- boost
44
- DoubleConversion
55
- fast_float
@@ -28,7 +28,7 @@ PODS:
2828
- SocketRocket
2929
- Yoga
3030
- boost (1.84.0)
31-
- CloudKitModule (1.1.15):
31+
- CloudKitModule (1.1.17):
3232
- boost
3333
- DoubleConversion
3434
- fast_float
@@ -66,7 +66,7 @@ PODS:
6666
- hermes-engine (0.14.0):
6767
- hermes-engine/Pre-built (= 0.14.0)
6868
- hermes-engine/Pre-built (0.14.0)
69-
- KeychainModule (1.1.15):
69+
- KeychainModule (1.1.17):
7070
- boost
7171
- DoubleConversion
7272
- fast_float
@@ -2653,7 +2653,7 @@ PODS:
26532653
- React-perflogger (= 0.83.0)
26542654
- React-utils (= 0.83.0)
26552655
- SocketRocket
2656-
- ReactNativeCheckBiometricAuthChanged (1.1.15):
2656+
- ReactNativeCheckBiometricAuthChanged (1.1.17):
26572657
- boost
26582658
- DoubleConversion
26592659
- fast_float
@@ -2683,7 +2683,7 @@ PODS:
26832683
- ReactCommon/turbomodule/core
26842684
- SocketRocket
26852685
- Yoga
2686-
- ReactNativeDeviceUtils (1.1.15):
2686+
- ReactNativeDeviceUtils (1.1.17):
26872687
- boost
26882688
- DoubleConversion
26892689
- fast_float
@@ -2713,7 +2713,7 @@ PODS:
27132713
- ReactCommon/turbomodule/core
27142714
- SocketRocket
27152715
- Yoga
2716-
- ReactNativeGetRandomValues (1.1.15):
2716+
- ReactNativeGetRandomValues (1.1.17):
27172717
- boost
27182718
- DoubleConversion
27192719
- fast_float
@@ -2743,7 +2743,7 @@ PODS:
27432743
- ReactCommon/turbomodule/core
27442744
- SocketRocket
27452745
- Yoga
2746-
- ReactNativeLiteCard (1.1.15):
2746+
- ReactNativeLiteCard (1.1.17):
27472747
- boost
27482748
- DoubleConversion
27492749
- fast_float
@@ -2771,7 +2771,7 @@ PODS:
27712771
- ReactCommon/turbomodule/core
27722772
- SocketRocket
27732773
- Yoga
2774-
- Skeleton (1.1.15):
2774+
- Skeleton (1.1.17):
27752775
- boost
27762776
- DoubleConversion
27772777
- fast_float
@@ -3077,16 +3077,16 @@ EXTERNAL SOURCES:
30773077
:path: "../../../node_modules/react-native/ReactCommon/yoga"
30783078

30793079
SPEC CHECKSUMS:
3080-
BackgroundThread: 8bfdbb6081771366de5e5e2b588aad1beafea9be
3080+
BackgroundThread: 22875378c238bbb3ae8e61c7aa8d0ebab67fe37c
30813081
boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90
3082-
CloudKitModule: a7596fae8e1e2d05bd0ba2fa4d4ad90b5e93ea5a
3082+
CloudKitModule: b2e3a96047478ada6a970ded4effb371ba3fbd99
30833083
DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb
30843084
fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6
30853085
FBLazyVector: a293a88992c4c33f0aee184acab0b64a08ff9458
30863086
fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd
30873087
glog: 5683914934d5b6e4240e497e0f4a3b42d1854183
30883088
hermes-engine: 70fdc9d0bb0d8532e0411dcb21e53ce5a160960a
3089-
KeychainModule: 7a82d9b61fbd70183fdb991384e6a99eff8ac502
3089+
KeychainModule: 4e4e8187e6b2ddbff6e2b6117e530ab5e50c3a5c
30903090
NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3
30913091
RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669
30923092
RCTDeprecation: 2b70c6e3abe00396cefd8913efbf6a2db01a2b36
@@ -3158,11 +3158,11 @@ SPEC CHECKSUMS:
31583158
ReactAppDependencyProvider: ebcf3a78dc1bcdf054c9e8d309244bade6b31568
31593159
ReactCodegen: 554b421c45b7df35ac791da1b734335470b55fcc
31603160
ReactCommon: 424cc34cf5055d69a3dcf02f3436481afb8b0f6f
3161-
ReactNativeCheckBiometricAuthChanged: 5757aec913dee03c7d488eebb6b05a91ff774e61
3162-
ReactNativeDeviceUtils: 2372c7f86169033f39b524b0bfc28323a2cf6bdc
3163-
ReactNativeGetRandomValues: 60a36d2a7e0ced4e37bbbc9faa98e11f27655518
3164-
ReactNativeLiteCard: d59be2b082bc3e7ef452095042c1e6b5483ab210
3165-
Skeleton: af32eaa6d321cf4f3266cf408dadec752dfb8e1d
3161+
ReactNativeCheckBiometricAuthChanged: 994b2c696a4c50dce9840c80944872ac3e9b24fd
3162+
ReactNativeDeviceUtils: b1312bad7e4899541d1cd0b5dc4b7db3ab6038f2
3163+
ReactNativeGetRandomValues: 84ed71f8e120d8019be1e1b623e4e3c9c78a88d9
3164+
ReactNativeLiteCard: 790a5dee05a01b83aa130c928645a74477c18279
3165+
Skeleton: b8a7888e09e33a9772ee87dd68db0dd64f46e832
31663166
SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
31673167
Yoga: 6ca93c8c13f56baeec55eb608577619b17a4d64e
31683168

example/react-native/pages/DeviceUtilsTestPage.tsx

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React, { useState, useRef, useEffect } from 'react';
22
import { View, Text, StyleSheet, Alert } from 'react-native';
33
import { TestPageBase, TestButton, TestInput, TestResult } from './TestPageBase';
44
import { ReactNativeDeviceUtils } from '@onekeyfe/react-native-device-utils';
@@ -20,6 +20,15 @@ export function DeviceUtilsTestPage({ onGoHome, safeAreaInsets }: DeviceUtilsTes
2020
const [colorA, setColorA] = useState('255');
2121
const [isSpanning, setIsSpanning] = useState(false);
2222
const [spanningCallbackActive, setSpanningCallbackActive] = useState(false);
23+
const spanningListenerIdRef = useRef<number | null>(null);
24+
25+
useEffect(() => {
26+
return () => {
27+
if (spanningListenerIdRef.current !== null) {
28+
deviceUtils.removeSpanningChangedListener(spanningListenerIdRef.current);
29+
}
30+
};
31+
}, []);
2332

2433

2534
// Clear previous results
@@ -115,14 +124,15 @@ export function DeviceUtilsTestPage({ onGoHome, safeAreaInsets }: DeviceUtilsTes
115124
clearResults();
116125
try {
117126
if (!spanningCallbackActive) {
118-
deviceUtils.addSpanningChangedListener((spanning: boolean) => {
127+
const listenerId = deviceUtils.addSpanningChangedListener((spanning: boolean) => {
119128
setIsSpanning(spanning);
120129
setResult({
121130
spanningCallbackTriggered: true,
122131
isSpanning: spanning,
123132
timestamp: new Date().toLocaleTimeString()
124133
});
125134
});
135+
spanningListenerIdRef.current = listenerId;
126136
setSpanningCallbackActive(true);
127137
Alert.alert('Success', 'Spanning callback registered! Try rotating your device or connecting/disconnecting external displays.');
128138
} else {
@@ -178,6 +188,20 @@ export function DeviceUtilsTestPage({ onGoHome, safeAreaInsets }: DeviceUtilsTes
178188
}
179189
};
180190

191+
// Test setUserInterfaceStyle
192+
const testSetUserInterfaceStyle = (style: 'light' | 'dark' | 'unspecified') => {
193+
clearResults();
194+
try {
195+
deviceUtils.setUserInterfaceStyle(style);
196+
setResult({
197+
userInterfaceStyleChanged: true,
198+
style: style,
199+
});
200+
} catch (err) {
201+
setError(err instanceof Error ? err.message : 'Unknown error');
202+
}
203+
};
204+
181205
// Get all device info at once
182206
const getAllDeviceInfo = async () => {
183207
clearResults();
@@ -335,6 +359,29 @@ export function DeviceUtilsTestPage({ onGoHome, safeAreaInsets }: DeviceUtilsTes
335359
</View>
336360
</View>
337361

362+
{/* User Interface Style Tests */}
363+
<View style={styles.section}>
364+
<Text style={styles.sectionTitle}>User Interface Style</Text>
365+
366+
<View style={styles.colorButtonsContainer}>
367+
<TestButton
368+
title="Light"
369+
onPress={() => testSetUserInterfaceStyle('light')}
370+
style={styles.colorButton}
371+
/>
372+
<TestButton
373+
title="Dark"
374+
onPress={() => testSetUserInterfaceStyle('dark')}
375+
style={styles.colorButton}
376+
/>
377+
<TestButton
378+
title="System"
379+
onPress={() => testSetUserInterfaceStyle('unspecified')}
380+
style={styles.colorButton}
381+
/>
382+
</View>
383+
</View>
384+
338385
{/* Results */}
339386
<TestResult result={result} error={error} />
340387

native-modules/react-native-background-thread/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-background-thread",
3-
"version": "1.1.16",
3+
"version": "1.1.18",
44
"description": "react-native-background-thread",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-modules/react-native-check-biometric-auth-changed/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-check-biometric-auth-changed",
3-
"version": "1.1.16",
3+
"version": "1.1.18",
44
"description": "react-native-check-biometric-auth-changed",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-modules/react-native-cloud-kit-module/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@onekeyfe/react-native-cloud-kit-module",
3-
"version": "1.1.16",
3+
"version": "1.1.18",
44
"description": "react-native-cloud-kit-module",
55
"main": "./lib/module/index.js",
66
"types": "./lib/typescript/src/index.d.ts",

native-modules/react-native-device-utils/android/build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,8 @@ dependencies {
129129
// WindowManager library for foldable device detection
130130
implementation "androidx.window:window:1.5.1"
131131
implementation "androidx.window:window-java:1.5.1"
132+
// AppCompat for night mode support
133+
implementation "androidx.appcompat:appcompat:1.7.1"
134+
// AndroidX Preference for PreferenceManager
135+
implementation "androidx.preference:preference-ktx:1.2.1"
132136
}

native-modules/react-native-device-utils/android/src/main/java/com/margelo/nitro/reactnativedeviceutils/ReactNativeDeviceUtils.kt

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import android.content.pm.PackageManager
66
import android.graphics.Color
77
import android.graphics.Rect
88
import android.os.Build
9-
import android.preference.PreferenceManager
9+
import androidx.preference.PreferenceManager
1010
import androidx.core.content.ContextCompat
1111
import androidx.core.util.Consumer
1212
import androidx.window.layout.FoldingFeature
1313
import androidx.window.layout.WindowInfoTracker
1414
import androidx.window.layout.WindowLayoutInfo
1515
import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter
16+
import androidx.appcompat.app.AppCompatDelegate
1617
import com.facebook.proguard.annotations.DoNotStrip
1718
import com.facebook.react.bridge.LifecycleEventListener
1819
import com.facebook.react.bridge.ReactApplicationContext
@@ -35,6 +36,7 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven
3536
*/
3637
companion object {
3738
private const val PREF_KEY_FOLDABLE = "1k_fold"
39+
private const val PREF_KEY_UI_STYLE = "1k_user_interface_style"
3840

3941
// Xiaomi foldable models
4042
private val XIAOMI_FOLDABLE_MODELS = setOf(
@@ -227,6 +229,29 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven
227229
NitroModules.applicationContext?.let { ctx ->
228230
ctx.addLifecycleEventListener(this)
229231
}
232+
restoreUserInterfaceStyle()
233+
}
234+
235+
private fun restoreUserInterfaceStyle() {
236+
try {
237+
val context = NitroModules.applicationContext ?: return
238+
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
239+
val style = prefs.getString(PREF_KEY_UI_STYLE, null) ?: return
240+
applyUserInterfaceStyle(style)
241+
} catch (e: Exception) {
242+
// Ignore restore errors
243+
}
244+
}
245+
246+
private fun applyUserInterfaceStyle(style: String) {
247+
val mode = when (style) {
248+
"light" -> AppCompatDelegate.MODE_NIGHT_NO
249+
"dark" -> AppCompatDelegate.MODE_NIGHT_YES
250+
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
251+
}
252+
android.os.Handler(android.os.Looper.getMainLooper()).post {
253+
AppCompatDelegate.setDefaultNightMode(mode)
254+
}
230255
}
231256

232257
private fun getCurrentActivity(): Activity? {
@@ -759,6 +784,26 @@ class ReactNativeDeviceUtils : HybridReactNativeDeviceUtilsSpec(), LifecycleEven
759784
}
760785
}
761786

787+
// MARK: - User Interface Style
788+
789+
override fun setUserInterfaceStyle(style: UserInterfaceStyle) {
790+
val styleString = when (style) {
791+
UserInterfaceStyle.LIGHT -> "light"
792+
UserInterfaceStyle.DARK -> "dark"
793+
UserInterfaceStyle.UNSPECIFIED -> "unspecified"
794+
}
795+
try {
796+
val context = NitroModules.applicationContext
797+
if (context != null) {
798+
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
799+
prefs.edit().putString(PREF_KEY_UI_STYLE, styleString).apply()
800+
}
801+
} catch (e: Exception) {
802+
// Ignore save errors
803+
}
804+
applyUserInterfaceStyle(styleString)
805+
}
806+
762807
// MARK: - Background Color
763808

764809
override fun changeBackgroundColor(r: Double, g: Double, b: Double, a: Double) {

native-modules/react-native-device-utils/ios/ReactNativeDeviceUtils.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,45 @@ import NitroModules
22
import UIKit
33

44
class ReactNativeDeviceUtils: HybridReactNativeDeviceUtilsSpec {
5-
5+
6+
private static let userInterfaceStyleKey = "1k_user_interface_style"
7+
8+
override init() {
9+
super.init()
10+
restoreUserInterfaceStyle()
11+
}
12+
13+
private func restoreUserInterfaceStyle() {
14+
guard let style = UserDefaults.standard.string(forKey: ReactNativeDeviceUtils.userInterfaceStyleKey) else {
15+
return
16+
}
17+
applyUserInterfaceStyle(style)
18+
}
19+
20+
private func applyUserInterfaceStyle(_ style: String) {
21+
DispatchQueue.main.async {
22+
var uiStyle: UIUserInterfaceStyle = .unspecified
23+
if style == "light" {
24+
uiStyle = .light
25+
} else if style == "dark" {
26+
uiStyle = .dark
27+
}
28+
if #available(iOS 15.0, *) {
29+
for scene in UIApplication.shared.connectedScenes {
30+
if let windowScene = scene as? UIWindowScene {
31+
for window in windowScene.windows {
32+
window.overrideUserInterfaceStyle = uiStyle
33+
}
34+
}
35+
}
36+
} else {
37+
for window in UIApplication.shared.windows {
38+
window.overrideUserInterfaceStyle = uiStyle
39+
}
40+
}
41+
}
42+
}
43+
644
public func isDualScreenDevice() throws -> Bool {
745
return false
846
}
@@ -31,6 +69,11 @@ class ReactNativeDeviceUtils: HybridReactNativeDeviceUtilsSpec {
3169
}
3270

3371

72+
public func setUserInterfaceStyle(style: UserInterfaceStyle) throws -> Void {
73+
UserDefaults.standard.set(style.stringValue, forKey: ReactNativeDeviceUtils.userInterfaceStyleKey)
74+
applyUserInterfaceStyle(style.stringValue)
75+
}
76+
3477
public func changeBackgroundColor(r: Double, g: Double, b: Double, a: Double) throws -> Void {
3578
DispatchQueue.main.async {
3679
// Clamp color values to valid range [0, 255]

0 commit comments

Comments
 (0)