├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .prettierrc ├── .watchmanconfig ├── LICENSE ├── README.md ├── android ├── .classpath ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── README.md ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── io │ └── expo │ └── appearance │ ├── RNCAppearanceModule.java │ └── RNCAppearancePackage.java ├── app.json ├── babel.config.js ├── example ├── .gitignore ├── App.tsx ├── android │ ├── app │ │ ├── _BUCK │ │ ├── build.gradle │ │ ├── build_defs.bzl │ │ ├── proguard-rules.pro │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── appearanceexample │ │ │ │ ├── MainActivity.java │ │ │ │ └── MainApplication.java │ │ │ └── res │ │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.png │ │ │ └── ic_launcher_round.png │ │ │ └── values │ │ │ ├── strings.xml │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── index.js ├── index.web.js └── ios │ ├── AppearanceExample-tvOS │ └── Info.plist │ ├── AppearanceExample-tvOSTests │ └── Info.plist │ ├── AppearanceExample.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ └── SafeAreaViewExample.xcscheme │ ├── AppearanceExample.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── AppearanceExample │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── main.m │ ├── Podfile │ └── Podfile.lock ├── ios ├── Appearance.xcodeproj │ └── project.pbxproj └── Appearance │ ├── RNCAppearance.h │ ├── RNCAppearance.m │ ├── RNCAppearanceProvider.h │ ├── RNCAppearanceProvider.m │ ├── RNCAppearanceProviderManager.h │ └── RNCAppearanceProviderManager.m ├── metro.config.js ├── package.json ├── react-native-appearance.podspec ├── src ├── @types │ └── use-subscription.d.ts ├── Appearance.types.ts ├── NativeAppearance.tsx ├── NativeAppearance.web.tsx ├── index.tsx ├── mock.tsx └── web │ ├── SyntheticPlatformEmitter.ts │ └── emitter-polyfill.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | typings 2 | node_modules 3 | example/android-bundle.js 4 | example/ios-bundle.js 5 | 6 | # generated by bob 7 | lib/ 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @format 8 | */ 9 | 10 | const typescriptEslintRecommended = require('@typescript-eslint/eslint-plugin/dist/configs/recommended.json'); 11 | const typescriptEslintPrettier = require('eslint-config-prettier/@typescript-eslint'); 12 | 13 | module.exports = { 14 | extends: ['@react-native-community'], 15 | overrides: [ 16 | { 17 | files: ['*.ts', '*.tsx'], 18 | // Apply the recommended Typescript defaults and the prettier overrides to all Typescript files 19 | rules: Object.assign( 20 | typescriptEslintRecommended.rules, 21 | typescriptEslintPrettier.rules, 22 | { 23 | '@typescript-eslint/explicit-member-accessibility': 'off', 24 | '@typescript-eslint/explicit-function-return-type': 'off', 25 | '@typescript-eslint/no-use-before-define': 'off', 26 | 'react-native/no-inline-styles': 'off', 27 | }, 28 | ), 29 | }, 30 | ], 31 | }; 32 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .expo 2 | 3 | # OSX 4 | # 5 | .DS_Store 6 | 7 | # node.js 8 | # 9 | node_modules/ 10 | npm-debug.log 11 | yarn-error.log 12 | 13 | 14 | # Xcode 15 | # 16 | build/ 17 | *.pbxuser 18 | !default.pbxuser 19 | *.mode1v3 20 | !default.mode1v3 21 | *.mode2v3 22 | !default.mode2v3 23 | *.perspectivev3 24 | !default.perspectivev3 25 | xcuserdata 26 | *.xccheckout 27 | *.moved-aside 28 | DerivedData 29 | *.hmap 30 | *.ipa 31 | *.xcuserstate 32 | project.xcworkspace 33 | 34 | 35 | # Android/IntelliJ 36 | # 37 | build/ 38 | .idea 39 | .gradle 40 | local.properties 41 | *.iml 42 | 43 | # BUCK 44 | buck-out/ 45 | \.buckd/ 46 | debug.keystore 47 | 48 | # Editor config 49 | .vscode 50 | 51 | # Outputs 52 | coverage 53 | 54 | .tmp 55 | example/android-bundle.js 56 | example/ios-bundle.js 57 | index.android.bundle 58 | index.ios.bundle 59 | 60 | # generated by bob 61 | lib/ 62 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "bracketSpacing": true, 5 | "jsxBracketSameLine": false, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-appearance 2 | 3 | Access operating system appearance information on iOS, Android, and web. Currently supports detecting preferred color scheme (light/dark). 4 | 5 | > ⚠️ [Appearance](https://reactnative.dev/docs/appearance) in React Native core is recommended unless you have a good reason to use the library (eg: you're on an older React Native version.) This project is archived now that it will not be needed going forward. 6 | 7 | ## Installation 8 | 9 | Installation instructions vary depending on whether you're using a managed Expo project or a bare React Native project. 10 | 11 | ### Managed Expo project 12 | 13 | This library is supported in Expo SDK 35+ (SDK 35 includes iOS support, SDK 36 and higher includes support for all platforms). 14 | 15 | ```sh 16 | expo install react-native-appearance 17 | ``` 18 | 19 | Then, in **app.json**, include `"userInterfaceStyle"` to listen to the device's appearance settings: 20 | 21 | ```js 22 | { 23 | "expo": { 24 | /* 25 | Supported user interface styles. If left blank, "light" will be used. Use "automatic" if you would like to support either "light" or "dark" depending on device settings. 26 | */ 27 | "userInterfaceStyle": "automatic" | "light" | "dark" 28 | } 29 | } 30 | ``` 31 | 32 | > Android support and web support are available on SDK36+. 33 | 34 | ### Bare React Native project 35 | 36 | ```sh 37 | yarn add react-native-appearance 38 | ``` 39 | 40 | ## Linking 41 | 42 | > If you are not using AndroidX on your project already (this is the default on React Native 0.60+, but not on lower versions) you will want to use the `jetifier` npm package. Install the package with `yarn add -D jetifier` and then under `scripts` add `"postinstall": "jetify -r"`. Next run `yarn jetify`. 43 | 44 | After installing the package you need to link the native parts of the library for the platforms you are using. The easiest way to link the library is using the CLI tool by running this command from the root of your project: 45 | 46 | ```sh 47 | react-native link react-native-appearance 48 | ``` 49 | 50 | If you can't or don't want to use the CLI tool, you can also manually link the library using the instructions below (click on the arrow to show them): 51 | 52 |
53 | Manually link the library on iOS 54 | 55 | Either follow the [instructions in the React Native documentation](https://facebook.github.io/react-native/docs/linking-libraries-ios#manual-linking) to manually link the framework or link using [Cocoapods](https://cocoapods.org) by adding this to your `Podfile`: 56 | 57 | ```ruby 58 | pod 'react-native-appearance', :path => '../node_modules/react-native-appearance' 59 | ``` 60 | 61 |
62 | 63 |
64 | Manually link the library on Android 65 | 66 | 1. Open up `android/app/src/main/java/[...]/MainApplication.java` 67 | 68 | - Add `import io.expo.appearance.RNCAppearancePackage;` to the imports at the top of the file 69 | - Add `new RNCAppearancePackage()` to the list returned by the `getPackages()` method 70 | 71 | 2. Append the following lines to `android/settings.gradle`: 72 | 73 | ``` 74 | include ':react-native-appearance' 75 | project(':react-native-appearance').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-appearance/android') 76 | 77 | ``` 78 | 79 | 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: 80 | 81 | ``` 82 | implementation project(':react-native-appearance') 83 | ``` 84 | 85 |
86 | 87 | ## Configuration 88 | 89 |
90 | iOS configuration 91 | 92 | In Expo managed projects, add `ios.userInterfaceStyle` to your `app.json`: 93 | 94 | ```json 95 | { 96 | "expo": { 97 | "ios": { 98 | "userInterfaceStyle": "automatic" 99 | } 100 | } 101 | } 102 | ``` 103 | 104 | For bare React Native apps, run `npx pod-install`. You can configure supported styles with the [UIUserInterfaceStyle](https://developer.apple.com/documentation/bundleresources/information_property_list/uiuserinterfacestyle) key in your app `Info.plist`. 105 | 106 |
107 | 108 |
109 | Android configuration 110 | 111 | Add the `uiMode` flag in `AndroidManifest.xml`: 112 | 113 | ```xml 114 | 117 | ``` 118 | 119 | Implement the `onConfigurationChanged` method in `MainActivity.java`: 120 | 121 | ```java 122 | import android.content.Intent; // <--- import 123 | import android.content.res.Configuration; // <--- import 124 | 125 | public class MainActivity extends ReactActivity { 126 | ...... 127 | 128 | // copy these lines 129 | @Override 130 | public void onConfigurationChanged(Configuration newConfig) { 131 | super.onConfigurationChanged(newConfig); 132 | Intent intent = new Intent("onConfigurationChanged"); 133 | intent.putExtra("newConfig", newConfig); 134 | sendBroadcast(intent); 135 | } 136 | 137 | ...... 138 | } 139 | ``` 140 | 141 |
142 | 143 | ## Usage 144 | 145 | First, you need to wrap your app in the `AppearanceProvider`. At the root of your app, do the following: 146 | 147 | ```js 148 | import { AppearanceProvider } from 'react-native-appearance'; 149 | 150 | export default () => ( 151 | 152 | 153 | 154 | ); 155 | ``` 156 | 157 | Now you can use `Appearance` and `useColorScheme` anywhere in your app. 158 | 159 | ```js 160 | import { Appearance, useColorScheme } from 'react-native-appearance'; 161 | 162 | /** 163 | * Get the current color scheme 164 | */ 165 | Appearance.getColorScheme(); 166 | 167 | /** 168 | * Subscribe to color scheme changes with a hook 169 | */ 170 | function MyComponent() { 171 | const colorScheme = useColorScheme(); 172 | if (colorScheme === 'dark') { 173 | // render some dark thing 174 | } else { 175 | // render some light thing 176 | } 177 | } 178 | 179 | /** 180 | * Subscribe to color scheme without a hook 181 | */ 182 | const subscription = Appearance.addChangeListener(({ colorScheme }) => { 183 | // do something with color scheme 184 | }); 185 | 186 | // Remove the subscription at some point 187 | subscription.remove(); 188 | ``` 189 | 190 | ## Attribution 191 | 192 | This was mostly written by Facebook for inclusion in React Native core. 193 | -------------------------------------------------------------------------------- /android/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android_ created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.buildship.core.gradleprojectbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.jdt.core.javanature 21 | org.eclipse.buildship.core.gradleprojectnature 22 | 23 | 24 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(5.6.1)) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home= 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /android/README.md: -------------------------------------------------------------------------------- 1 | README 2 | ====== 3 | 4 | If you want to publish the lib as a maven dependency, follow these steps before publishing a new version to npm: 5 | 6 | 1. Be sure to have the Android [SDK](https://developer.android.com/studio/index.html) and [NDK](https://developer.android.com/ndk/guides/index.html) installed 7 | 2. Be sure to have a `local.properties` file in this folder that points to the Android SDK and NDK 8 | ``` 9 | ndk.dir=/Users/{username}/Library/Android/sdk/ndk-bundle 10 | sdk.dir=/Users/{username}/Library/Android/sdk 11 | ``` 12 | 3. Delete the `maven` folder 13 | 4. Run `./gradlew installArchives` 14 | 5. Verify that latest set of generated files is in the maven folder with the correct version number 15 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | // android/build.gradle 2 | 3 | def safeExtGet(prop, fallback) { 4 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 5 | } 6 | 7 | buildscript { 8 | // The Android Gradle plugin is only required when opening the android folder stand-alone. 9 | // This avoids unnecessary downloads and potential conflicts when the library is included as a 10 | // module dependency in an application project. 11 | if (project == rootProject) { 12 | repositories { 13 | google() 14 | jcenter() 15 | } 16 | dependencies { 17 | classpath 'com.android.tools.build:gradle:3.4.1' 18 | } 19 | } 20 | } 21 | 22 | apply plugin: 'com.android.library' 23 | apply plugin: 'maven' 24 | 25 | // Matches values in recent template from React Native 0.59 / 0.60 26 | // https://github.com/facebook/react-native/blob/0.59-stable/template/android/build.gradle#L5-L9 27 | // https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle#L5-L9 28 | def DEFAULT_COMPILE_SDK_VERSION = 28 29 | def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3" 30 | def DEFAULT_MIN_SDK_VERSION = 16 31 | def DEFAULT_TARGET_SDK_VERSION = 28 32 | 33 | android { 34 | compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION) 35 | buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION) 36 | defaultConfig { 37 | minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION) 38 | targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION) 39 | versionCode 1 40 | versionName "1.0" 41 | } 42 | lintOptions { 43 | abortOnError false 44 | } 45 | } 46 | 47 | repositories { 48 | mavenLocal() 49 | maven { 50 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 51 | url "$rootDir/../node_modules/react-native/android" 52 | } 53 | maven { 54 | // Android JSC is installed from npm 55 | url "$rootDir/../node_modules/jsc-android/dist" 56 | } 57 | google() 58 | jcenter() 59 | } 60 | 61 | dependencies { 62 | // ref: 63 | // https://github.com/facebook/react-native/blob/0.61-stable/template/android/app/build.gradle#L192 64 | //noinspection GradleDynamicVersion 65 | implementation 'com.facebook.react:react-native:+' // From node_modules 66 | } 67 | 68 | def configureReactNativePom(def pom) { 69 | def packageJson = new groovy.json.JsonSlurper().parseText(file('../package.json').text) 70 | 71 | pom.project { 72 | name packageJson.title 73 | artifactId packageJson.name 74 | version = packageJson.version 75 | group = "io.expo.appearance" 76 | description packageJson.description 77 | url packageJson.repository.baseUrl 78 | 79 | licenses { 80 | license { 81 | name packageJson.license 82 | url packageJson.repository.baseUrl + '/blob/master/' + packageJson.licenseFilename 83 | distribution 'repo' 84 | } 85 | } 86 | 87 | developers { 88 | developer { 89 | id "brentvatne" 90 | name "Brent Vatne" 91 | } 92 | } 93 | } 94 | } 95 | 96 | afterEvaluate { project -> 97 | // some Gradle build hooks ref: 98 | // https://www.oreilly.com/library/view/gradle-beyond-the/9781449373801/ch03.html 99 | task androidJavadoc(type: Javadoc) { 100 | source = android.sourceSets.main.java.srcDirs 101 | classpath += files(android.bootClasspath) 102 | classpath += files(project.getConfigurations().getByName('compile').asList()) 103 | include '**/*.java' 104 | } 105 | 106 | task androidJavadocJar(type: Jar, dependsOn: androidJavadoc) { 107 | classifier = 'javadoc' 108 | from androidJavadoc.destinationDir 109 | } 110 | 111 | task androidSourcesJar(type: Jar) { 112 | classifier = 'sources' 113 | from android.sourceSets.main.java.srcDirs 114 | include '**/*.java' 115 | } 116 | 117 | android.libraryVariants.all { variant -> 118 | def name = variant.name.capitalize() 119 | task "jar${name}"(type: Jar, dependsOn: variant.javaCompileProvider.get()) { 120 | from variant.javaCompileProvider.get().destinationDir 121 | } 122 | } 123 | 124 | artifacts { 125 | archives androidSourcesJar 126 | archives androidJavadocJar 127 | } 128 | 129 | task installArchives(type: Upload) { 130 | configuration = configurations.archives 131 | repositories.mavenDeployer { 132 | // Deploy to react-native-event-bridge/maven, ready to publish to npm 133 | repository url: "file://${projectDir}/../android/maven" 134 | configureReactNativePom pom 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/java/io/expo/appearance/RNCAppearanceModule.java: -------------------------------------------------------------------------------- 1 | package io.expo.appearance; 2 | 3 | import android.app.Activity; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import android.content.res.Configuration; 9 | import android.os.Build; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | 14 | import com.facebook.common.logging.FLog; 15 | import com.facebook.react.bridge.Arguments; 16 | import com.facebook.react.bridge.LifecycleEventListener; 17 | import com.facebook.react.bridge.ReactApplicationContext; 18 | import com.facebook.react.bridge.ReactContext; 19 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 20 | import com.facebook.react.bridge.WritableMap; 21 | import com.facebook.react.common.ReactConstants; 22 | import com.facebook.react.modules.core.DeviceEventManagerModule; 23 | 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | 27 | public class RNCAppearanceModule extends ReactContextBaseJavaModule implements LifecycleEventListener { 28 | public static final String REACT_CLASS = "RNCAppearance"; 29 | private BroadcastReceiver mBroadcastReceiver = null; 30 | 31 | public RNCAppearanceModule(@NonNull ReactApplicationContext reactContext) { 32 | super(reactContext); 33 | // Only Android 10+ supports dark mode 34 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { 35 | final ReactApplicationContext ctx = reactContext; 36 | mBroadcastReceiver = new BroadcastReceiver() { 37 | @Override 38 | public void onReceive(Context context, Intent intent) { 39 | Configuration newConfig = intent.getParcelableExtra("newConfig"); 40 | sendEvent(ctx, "appearanceChanged", getPreferences()); 41 | } 42 | }; 43 | ctx.addLifecycleEventListener(this); 44 | } 45 | } 46 | 47 | @NonNull 48 | @Override 49 | public String getName() { 50 | return REACT_CLASS; 51 | } 52 | 53 | // `protected` to allow overriding in Expo client for scoping purposes 54 | protected String getColorScheme(Configuration config) { 55 | String colorScheme = "no-preference"; 56 | 57 | // Only Android 10+ support dark mode 58 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { 59 | int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; 60 | switch (currentNightMode) { 61 | case Configuration.UI_MODE_NIGHT_NO: 62 | case Configuration.UI_MODE_NIGHT_UNDEFINED: 63 | colorScheme = "light"; 64 | break; 65 | case Configuration.UI_MODE_NIGHT_YES: 66 | colorScheme = "dark"; 67 | break; 68 | 69 | } 70 | } 71 | 72 | return colorScheme; 73 | } 74 | 75 | private WritableMap getPreferences() { 76 | WritableMap preferences = Arguments.createMap(); 77 | 78 | // Attempt to use the Activity context first in order to get the most up to date 79 | // scheme. This covers the scenario when AppCompatDelegate.setDefaultNightMode() 80 | // is called directly (which can occur in Brownfield apps for example). 81 | Activity activity = getCurrentActivity(); 82 | Context context = activity != null ? activity : getReactApplicationContext(); 83 | 84 | String colorScheme = getColorScheme(context.getResources().getConfiguration()); 85 | preferences.putString("colorScheme", colorScheme); 86 | return preferences; 87 | } 88 | 89 | @Nullable 90 | @Override 91 | public Map getConstants() { 92 | HashMap constants = new HashMap(); 93 | constants.put("initialPreferences", getPreferences()); 94 | return constants; 95 | } 96 | 97 | private void sendEvent(ReactContext reactContext, String eventName, @Nullable WritableMap params) { 98 | if (reactContext.hasActiveCatalystInstance()) { 99 | FLog.i("sendEvent", eventName + ": " + params.toString()); 100 | reactContext 101 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) 102 | .emit(eventName, params); 103 | } 104 | 105 | } 106 | 107 | private void sendEvent(String eventName, @Nullable WritableMap params) { 108 | if (getReactApplicationContext().hasActiveCatalystInstance()) { 109 | FLog.i("sendEvent", eventName + ": " + params.toString()); 110 | getReactApplicationContext() 111 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) 112 | .emit(eventName, params); 113 | } 114 | 115 | } 116 | 117 | // We don't do any validation on whether the appearance has actually changed since the last 118 | // event was sent. We ignore this on the JS side if unchanged. 119 | private void updateAndSendAppearancePreferences() { 120 | WritableMap preferences = getPreferences(); 121 | sendEvent("appearanceChanged", preferences); 122 | } 123 | 124 | @Override 125 | public void onHostResume() { 126 | final Activity activity = getCurrentActivity(); 127 | 128 | if (activity == null) { 129 | FLog.e(ReactConstants.TAG, "no activity to register receiver"); 130 | return; 131 | } 132 | activity.registerReceiver(mBroadcastReceiver, new IntentFilter("onConfigurationChanged")); 133 | 134 | // Send updated preferences to JS when the app is resumed, because we don't receive updates 135 | // when backgrounded 136 | updateAndSendAppearancePreferences(); 137 | } 138 | 139 | @Override 140 | public void onHostPause() { 141 | final Activity activity = getCurrentActivity(); 142 | if (activity == null) return; 143 | try { 144 | activity.unregisterReceiver(mBroadcastReceiver); 145 | } catch (java.lang.IllegalArgumentException e) { 146 | FLog.e(ReactConstants.TAG, "mBroadcastReceiver already unregistered", e); 147 | } 148 | } 149 | 150 | @Override 151 | public void onHostDestroy() { 152 | // No need to do anything as far as I know? 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /android/src/main/java/io/expo/appearance/RNCAppearancePackage.java: -------------------------------------------------------------------------------- 1 | package io.expo.appearance; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.facebook.react.ReactPackage; 6 | import com.facebook.react.bridge.NativeModule; 7 | import com.facebook.react.bridge.ReactApplicationContext; 8 | import com.facebook.react.uimanager.ViewManager; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.List; 14 | 15 | public class RNCAppearancePackage implements ReactPackage { 16 | @NonNull 17 | @Override 18 | public List createNativeModules(@NonNull ReactApplicationContext reactContext) { 19 | List modules = new ArrayList(); 20 | modules.add(new RNCAppearanceModule(reactContext)); 21 | return modules; 22 | } 23 | 24 | @Override 25 | @SuppressWarnings("rawtypes") 26 | public List createViewManagers(ReactApplicationContext reactContext) { 27 | return Collections.emptyList(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AppearanceExample", 3 | "displayName": "AppearanceExample", 4 | "expo": { 5 | "entryPoint": "./example/index.web.js", 6 | "sdkVersion": "34.0.0", 7 | "name": "react-native-appearance", 8 | "slug": "react-native-appearance", 9 | "version": "0.1.0", 10 | "platforms": ["web"], 11 | "web": { 12 | "display": "fullscreen", 13 | "barStyle": "black-translucent" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Expo 6 | # 7 | .expo 8 | 9 | # Xcode 10 | # 11 | build/ 12 | *.pbxuser 13 | !default.pbxuser 14 | *.mode1v3 15 | !default.mode1v3 16 | *.mode2v3 17 | !default.mode2v3 18 | *.perspectivev3 19 | !default.perspectivev3 20 | xcuserdata 21 | *.xccheckout 22 | *.moved-aside 23 | DerivedData 24 | *.hmap 25 | *.ipa 26 | *.xcuserstate 27 | project.xcworkspace 28 | 29 | # Android/IntelliJ 30 | # 31 | build/ 32 | .idea 33 | .gradle 34 | local.properties 35 | *.iml 36 | 37 | # node.js 38 | # 39 | node_modules/ 40 | npm-debug.log 41 | yarn-error.log 42 | 43 | # BUCK 44 | buck-out/ 45 | \.buckd/ 46 | *.keystore 47 | 48 | # fastlane 49 | # 50 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 51 | # screenshots whenever they are needed. 52 | # For more information about the recommended setup visit: 53 | # https://docs.fastlane.tools/best-practices/source-control/ 54 | 55 | */fastlane/report.xml 56 | */fastlane/Preview.html 57 | */fastlane/screenshots 58 | 59 | # Bundle artifact 60 | *.jsbundle 61 | 62 | # CocoaPods 63 | /ios/Pods/ 64 | -------------------------------------------------------------------------------- /example/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { StyleSheet, Text, View, Button } from 'react-native'; 3 | 4 | import { Appearance, AppearanceProvider, useColorScheme } from '..'; 5 | 6 | export default () => { 7 | const colorScheme = useColorScheme(); 8 | const isDark = colorScheme === 'dark'; 9 | 10 | const color = isDark ? '#f1f1f1' : '#333'; 11 | return ( 12 | 13 | 14 | {colorScheme} 15 |