Skip to content

Commit

Permalink
feat(webapis): introduce @react-native-webapis/battery-status
Browse files Browse the repository at this point in the history
  • Loading branch information
tido64 committed Aug 8, 2023
1 parent 7609d4a commit 9f50b41
Show file tree
Hide file tree
Showing 24 changed files with 456 additions and 9 deletions.
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 @@
---
---
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/incubator/*/bin/
/incubator/*/dist/
/incubator/*/lib/
/incubator/@react-native-webapis/*/lib/
/packages/*/*.LICENSE.txt
/packages/*/*/rnx-build/
/packages/*/bin/
Expand Down
92 changes: 92 additions & 0 deletions incubator/@react-native-webapis/battery-status/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# @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.

## Motivation

This is a prototype for the [React Native WebAPIs RFC](https://github.com/microsoft/rnx-kit/pull/2504)

## 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/metro.config.js b/packages/test-app/metro.config.js
index 7c0dcfc2..df0f8b0d 100644
--- a/packages/test-app/metro.config.js
+++ b/packages/test-app/metro.config.js
@@ -33,4 +33,7 @@ module.exports = makeMetroConfig({
blacklistRE: blockList,
blockList,
},
+ serializer: {
+ getModulesRunBeforeMainModule: require("@rnx-kit/polyfills").default,
+ },
});
diff --git a/packages/test-app/src/App.native.tsx b/packages/test-app/src/App.native.tsx
index 599634a9..b465f0fe 100644
--- a/packages/test-app/src/App.native.tsx
+++ b/packages/test-app/src/App.native.tsx
@@ -1,3 +1,5 @@
+// Temporary until we figure out how to magically inject WebAPIs
+import "@react-native-webapis/battery-status";
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
+ global.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,30 @@
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.source_files = 'ios/*.{h,m}', # :path
"#{repository['directory']}/ios/*.{h,m}" # :podspec
s.public_header_files = 'ios/*.h', # :path
"#{repository['directory']}/ios/*.h" # :podspec
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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
}

ext.kotlinVersion = getExtProp("kotlinVersion", "1.7.21")

repositories {
google()
mavenCentral()
}

dependencies {
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 {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion"

//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
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#import "RNWBatteryStatus.h"

@implementation RNWBatteryStatus

RCT_EXPORT_MODULE(RNWBatteryStatus)

+ (BOOL)requiresMainQueueSetup
{
return NO;
}

// clang-format off
RCT_EXPORT_METHOD(getStatus:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
// clang-format on
{
UIDevice *device = [UIDevice currentDevice];
[device setBatteryMonitoringEnabled:YES];

UIDeviceBatteryState batteryState = [device batteryState];
BOOL isCharging = batteryState == UIDeviceBatteryStateCharging;
NSDictionary *status = @{
@"charging": [NSNumber numberWithBool:isCharging],
@"chargingTime":
[NSNumber numberWithFloat:batteryState == UIDeviceBatteryStateFull ? 0 : -1],
@"dischargingTime": [NSNumber numberWithFloat:-1],
@"level": [NSNumber numberWithFloat:[device batteryLevel]],
};
[device setBatteryMonitoringEnabled:NO];

resolve(status);
}

@end
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/* Dummy file so @react-native-community/cli recognizes this as an iOS package */
Loading

0 comments on commit 9f50b41

Please sign in to comment.