Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webapis): introduce @react-native-webapis/battery-status #2590

Merged
merged 22 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/smart-vans-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
18 changes: 10 additions & 8 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
'chore':
"chore":
- .github/**/*
- .gitignore
- .npmrc
- .yarnrc
- package.json
- scripts/**/*
'feature: align-deps':
"feature: align-deps":
- packages/align-deps/**/*
'feature: cli':
"feature: cli":
- packages/cli/**/*
- packages/config/**/*
'feature: eslint':
"feature: eslint":
- packages/eslint-*/**/*
'feature: jest':
"feature: jest":
- packages/jest-*/**/*
'feature: metro':
"feature: metro":
- packages/babel-plugin-import-path-remapper/**/*
- packages/babel-preset-metro-react-native/**/*
- packages/metro-*/**/*
- packages/typescript-service/**/*
'feature: sdk':
"feature: sdk":
- packages/react-native-*/**/*
'feature: third party notices':
"feature: third party notices":
- packages/third-party-notices/**/*
"feature: webapis":
- "incubator/@react-native-webapis/**/*"
10 changes: 8 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
**/target/CACHEDIR.TAG
**/target/debug/
**/target/release/
**/windows/AppPackages/
**/windows/Generated Files/
**/windows/obj/
**/windows/x64/
*.log
*.tgz
*.xcworkspace/
Expand All @@ -19,17 +23,19 @@
!.yarn/releases/
/incubator/*/dist/
/incubator/*/lib/
/incubator/@react-native-webapis/*/lib/
/packages/*/*.LICENSE.txt
/packages/*/*/rnx-build/
/packages/*/dist/
/packages/*/ios/build
/packages/*/ios/build/
/packages/*/lib/
/packages/*/macos/build
/packages/*/macos/build/
/packages/template/CHANGELOG.md
/scripts/bin/
/scripts/lib/
Pods/
coverage/
local.properties
msbuild.binlog
node_modules/
!**/__fixtures__/**/node_modules/
97 changes: 97 additions & 0 deletions incubator/@react-native-webapis/battery-status/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# @react-native-webapis/battery-status

[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml)
[![npm version](https://img.shields.io/npm/v/@react-native-webapis/battery-status)](https://www.npmjs.com/package/@react-native-webapis/battery-status)

🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧

### THIS TOOL IS EXPERIMENTAL — USE WITH CAUTION

🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧🚧

[Battery Status API](https://developer.mozilla.org/en-US/docs/Web/API/Battery_Status_API)
for React Native.

> **Note**
>
> This is purely a prototype for the
> [React Native WebAPIs RFC](https://github.com/microsoft/rnx-kit/pull/2504). It
> currently does not implement the Battery Status API to spec, e.g. it's missing
> events and its properties are not updated live. Rather, its purpose is to show
> that it's possible to have a native module polyfill a web API, enabling direct
> use of previously web-only code in a React Native app.

## Installation

```sh
yarn add @rnx-kit/polyfills --dev
yarn add @react-native-webapis/battery-status
```

or if you're using npm

```sh
npm add --save-dev @rnx-kit/polyfills
npm add @react-native-webapis/battery-status
```

## Usage

```diff
diff --git a/packages/test-app/babel.config.js b/packages/test-app/babel.config.js
index 69ebd557..a012b7f5 100644
--- a/packages/test-app/babel.config.js
+++ b/packages/test-app/babel.config.js
@@ -13,6 +13,7 @@ module.exports = {
{ runtime: "automatic" },
],
[require("@babel/plugin-transform-react-jsx-source")],
+ [require("@rnx-kit/polyfills")],
],
},
],
diff --git a/packages/test-app/src/App.native.tsx b/packages/test-app/src/App.native.tsx
index 599634a9..a9b493ab 100644
--- a/packages/test-app/src/App.native.tsx
+++ b/packages/test-app/src/App.native.tsx
@@ -1,3 +1,5 @@
+// @react-native-webapis
+
import { acquireTokenWithScopes } from "@rnx-kit/react-native-auth";
// Both `internal` imports are used to verify that `metro-resolver-symlinks`
// resolves them correctly when `experimental_retryResolvingFromDisk` is
@@ -7,7 +9,7 @@ import {
getRemoteDebuggingAvailability,
} from "internal";
import { getHermesVersion } from "internal/hermes";
-import React, { useCallback, useMemo, useState } from "react";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
import type { LayoutChangeEvent } from "react-native";
import {
NativeModules,
@@ -186,6 +188,14 @@ function App({ concurrentRoot }: { concurrentRoot?: boolean }) {
[setFabric]
);

+ const [batteryLevel, setBatteryLevel] = useState(-1);
+ useEffect(() => {
+ // @ts-expect-error FIXME
+ navigator.getBattery().then((status) => {
+ setBatteryLevel(status.level);
+ });
+ }, []);
+
return (
<SafeAreaView style={styles.body}>
<StatusBar barStyle={isDarkMode ? "light-content" : "dark-content"} />
@@ -195,6 +205,9 @@ function App({ concurrentRoot }: { concurrentRoot?: boolean }) {
style={styles.body}
>
<Header />
+ <View style={styles.group}>
+ <Feature value={batteryLevel.toFixed(2)}>Battery Level</Feature>
+ </View>
<View style={styles.group}>
<Button onPress={startAcquireToken}>Acquire Token</Button>
</View>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
require 'json'

package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
version = package['version']
repository = package['repository']

Pod::Spec.new do |s|
s.name = 'RNWBatteryStatus'
s.version = version
s.author = { package['author']['name'] => package['author']['email'] }
s.license = package['license']
s.homepage = package['homepage']
s.source = { :git => repository['url'], :tag => "#{package['name']}@#{version}" }
s.summary = package['description']

s.ios.deployment_target = '13.0'
s.osx.deployment_target = '10.15'

s.dependency 'React-Core'

s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }

# Include both package and repository relative paths to allow the podspec to
# be consumed from both a local path, and as a podspec outside a spec
# repository.
s.ios.source_files = 'ios/*.{h,m}', # :path
"#{repository['directory']}/ios/*.{h,m}" # :podspec
s.ios.public_header_files = 'ios/*.h', # :path
"#{repository['directory']}/ios/*.h" # :podspec
s.osx.source_files = 'macos/*.{h,m}', # :path
"#{repository['directory']}/macos/*.{h,m}" # :podspec
s.osx.public_header_files = 'macos/*.h', # :path
"#{repository['directory']}/macos/*.h" # :podspec
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import java.nio.file.Paths

buildscript {
ext.findFile = { fileName ->
def currentDirPath = rootDir == null ? null : rootDir.toString()

while (currentDirPath != null) {
def currentDir = file(currentDirPath);
def requestedFile = Paths.get(currentDirPath, fileName).toFile()

if (requestedFile.exists()) {
return requestedFile
}

currentDirPath = currentDir.getParent()
}

return null
}

ext.findNodeModulesPath = { packageName ->
return findFile(Paths.get("node_modules", packageName).toString())
}

ext.getExtProp = { prop, defaultValue ->
return rootProject.ext.has(prop) ? rootProject.ext.get(prop) : defaultValue
}

repositories {
google()
mavenCentral()
}

dependencies {
def kotlinVersion = getExtProp("kotlinVersion", "1.7.21")
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
}
}

plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}

repositories {
maven {
url("${findNodeModulesPath('react-native')}/android")
}

google()
mavenCentral()
}

android {
compileSdkVersion getExtProp("compileSdkVersion", 33)
defaultConfig {
minSdkVersion getExtProp("minSdkVersion", 23)
targetSdkVersion getExtProp("targetSdkVersion", 29)
}
lintOptions {
abortOnError false
}
}

dependencies {
//noinspection GradleDynamicVersion
implementation "com.facebook.react:react-native:+"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# These properties are required to enable AndroidX for the test app.
android.useAndroidX=true
android.enableJetifier=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.reactnativewebapis.batterystatus">
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.reactnativewebapis.batterystatus

import android.content.Context
import android.os.BatteryManager
import android.os.Build
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReactModuleWithSpec

class BatteryStatusModule(context: ReactApplicationContext?) :
ReactContextBaseJavaModule(context), ReactModuleWithSpec {

companion object {
const val NAME = "RNWBatteryStatus"
}

override fun getName(): String = NAME

@ReactMethod
fun getStatus(promise: Promise) {
val batteryManager =
reactApplicationContext.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
promise.resolve(
Arguments.createMap().also { map ->
map.putBoolean("charging", batteryManager.isCharging)
map.putInt("chargingTime", batteryManager.getChargingTime())
map.putInt("dischargingTime", -1)
map.putDouble("level", batteryManager.getBatteryLevel())
}
)
}
}

fun BatteryManager.getBatteryLevel(): Double {
return getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) / 100.0
}

fun BatteryManager.getChargingTime(): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
(computeChargeTimeRemaining() / 1000).toInt()
} else {
-1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.reactnativewebapis.batterystatus

import com.facebook.react.TurboReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class BatteryStatusPackage : TurboReactPackage() {
override fun getModule(name: String?, reactContext: ReactApplicationContext?): NativeModule {
return when (name) {
BatteryStatusModule.NAME -> BatteryStatusModule(reactContext)
else -> throw IllegalArgumentException("No module named '$name'")
}
}

override fun getReactModuleInfoProvider(): ReactModuleInfoProvider =
ReactModuleInfoProvider {
val info = ReactModuleInfo(
BatteryStatusModule.NAME,
BatteryStatusModule::class.java.name,
false,
false,
false,
false,
false
)
mapOf(info.name() to info).toMutableMap()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#import <Foundation/Foundation.h>

#import <React/RCTBridgeModule.h>

NS_ASSUME_NONNULL_BEGIN

@interface RNWBatteryStatus : NSObject <RCTBridgeModule>
@end

NS_ASSUME_NONNULL_END
Loading