react-native-device-posture 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/android/build.gradle +62 -0
- package/android/gradle.properties +4 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/deviceposture/DevicePostureModule.kt +131 -0
- package/android/src/main/java/com/deviceposture/DevicePosturePackage.kt +32 -0
- package/ios/DevicePostureModule.h +1 -0
- package/ios/DevicePostureModule.mm +164 -0
- package/package.json +43 -0
- package/react-native-device-posture.podspec +35 -0
- package/react-native.config.js +8 -0
- package/src/FoldStateProvider.js +69 -0
- package/src/NativeDevicePosture.ts +21 -0
- package/src/constants.js +17 -0
- package/src/index.js +12 -0
- package/src/normalizeFoldState.js +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# react-native-device-posture
|
|
2
|
+
|
|
3
|
+
One fold and hinge status for iOS and Android. Wrap the app once, then read the same state from any screen.
|
|
4
|
+
|
|
5
|
+
Both platforms emit the listener `onFoldStateChange`.
|
|
6
|
+
|
|
7
|
+
| `status` | Meaning | `isFlat` |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| `unknown` | This device has no hinge | `false` |
|
|
10
|
+
| `closed` | Folded | `false` |
|
|
11
|
+
| `partiallyOpen` | Mid-fold | `false` |
|
|
12
|
+
| `fullyOpen` | Unfolded flat | `true` |
|
|
13
|
+
|
|
14
|
+
`isFlat` is true only when the device is fully open. Use that for the wide Duo layout.
|
|
15
|
+
|
|
16
|
+
iOS reads `UIHinge` (iOS 27.1 and later). Android reads Jetpack WindowManager `FoldingFeature`. Phones without a hinge stay on `unknown`.
|
|
17
|
+
|
|
18
|
+
Requires React Native 0.76 or newer with the New Architecture enabled.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
From the app:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install /Users/shubham/Documents/Learning/react-native-device-posture
|
|
26
|
+
cd ios && pod install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Rebuild the native app after installing. A JavaScript reload is not enough.
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
```jsx
|
|
34
|
+
import {
|
|
35
|
+
FoldStateProvider,
|
|
36
|
+
useFoldState,
|
|
37
|
+
FOLD_STATE_EVENT,
|
|
38
|
+
FOLD_STATUS,
|
|
39
|
+
} from 'react-native-device-posture';
|
|
40
|
+
|
|
41
|
+
function App() {
|
|
42
|
+
return (
|
|
43
|
+
<FoldStateProvider>
|
|
44
|
+
<Root />
|
|
45
|
+
</FoldStateProvider>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function Home() {
|
|
50
|
+
const { isFoldSupported, isFlat, status } = useFoldState();
|
|
51
|
+
const isDuoOpen = isFoldSupported && isFlat;
|
|
52
|
+
|
|
53
|
+
return isDuoOpen ? <WideHome /> : <PhoneHome />;
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`FOLD_STATE_EVENT` is `'onFoldStateChange'` on both platforms. The provider already subscribes to it, so screens only need `useFoldState()`.
|
|
58
|
+
|
|
59
|
+
`useFoldState()` throws if it is called outside `FoldStateProvider`.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : project.properties["DevicePosture_kotlinVersion"]
|
|
3
|
+
|
|
4
|
+
repositories {
|
|
5
|
+
google()
|
|
6
|
+
mavenCentral()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
dependencies {
|
|
10
|
+
classpath "com.android.tools.build:gradle:8.7.2"
|
|
11
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
def isNewArchitectureEnabled() {
|
|
16
|
+
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
apply plugin: "com.android.library"
|
|
20
|
+
apply plugin: "kotlin-android"
|
|
21
|
+
|
|
22
|
+
if (isNewArchitectureEnabled()) {
|
|
23
|
+
apply plugin: "com.facebook.react"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def getExtOrDefault(name) {
|
|
27
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["DevicePosture_" + name]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def getExtOrIntegerDefault(name) {
|
|
31
|
+
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["DevicePosture_" + name]).toInteger()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "com.deviceposture"
|
|
36
|
+
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
|
|
37
|
+
|
|
38
|
+
defaultConfig {
|
|
39
|
+
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
|
|
40
|
+
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
compileOptions {
|
|
44
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
45
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
kotlinOptions {
|
|
49
|
+
jvmTarget = "17"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
repositories {
|
|
54
|
+
mavenCentral()
|
|
55
|
+
google()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
dependencies {
|
|
59
|
+
implementation "com.facebook.react:react-android"
|
|
60
|
+
implementation "androidx.window:window:1.3.0"
|
|
61
|
+
implementation "androidx.window:window-java:1.3.0"
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
package com.deviceposture
|
|
2
|
+
|
|
3
|
+
import android.content.pm.PackageManager
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import androidx.core.util.Consumer
|
|
6
|
+
import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter
|
|
7
|
+
import androidx.window.layout.FoldingFeature
|
|
8
|
+
import androidx.window.layout.WindowInfoTracker
|
|
9
|
+
import androidx.window.layout.WindowLayoutInfo
|
|
10
|
+
import com.facebook.react.bridge.Arguments
|
|
11
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
12
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
13
|
+
import com.facebook.react.bridge.WritableMap
|
|
14
|
+
import java.util.concurrent.ExecutorService
|
|
15
|
+
import java.util.concurrent.Executors
|
|
16
|
+
|
|
17
|
+
class DevicePostureModule(reactContext: ReactApplicationContext) :
|
|
18
|
+
NativeDevicePostureSpec(reactContext), LifecycleEventListener {
|
|
19
|
+
|
|
20
|
+
private data class Posture(
|
|
21
|
+
val isFoldSupported: Boolean,
|
|
22
|
+
val status: String,
|
|
23
|
+
val isFlat: Boolean,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
|
|
27
|
+
private var tracker: WindowInfoTrackerCallbackAdapter? = null
|
|
28
|
+
private var listening = false
|
|
29
|
+
|
|
30
|
+
@Volatile private var posture = Posture(false, "unknown", false)
|
|
31
|
+
|
|
32
|
+
private val layoutCallback =
|
|
33
|
+
Consumer<WindowLayoutInfo> { info ->
|
|
34
|
+
publish(info)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
init {
|
|
38
|
+
if (hasHinge()) {
|
|
39
|
+
tracker = WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(reactContext))
|
|
40
|
+
posture = Posture(true, "unknown", false)
|
|
41
|
+
}
|
|
42
|
+
reactContext.addLifecycleEventListener(this)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
override fun getName(): String = NAME
|
|
46
|
+
|
|
47
|
+
override fun getFoldState(): WritableMap = posture.toMap()
|
|
48
|
+
|
|
49
|
+
override fun startListening() {
|
|
50
|
+
listening = true
|
|
51
|
+
attachListener()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
override fun stopListening() {
|
|
55
|
+
listening = false
|
|
56
|
+
detachListener()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
override fun onHostResume() {
|
|
60
|
+
if (listening) {
|
|
61
|
+
attachListener()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
override fun onHostPause() = Unit
|
|
66
|
+
|
|
67
|
+
override fun onHostDestroy() {
|
|
68
|
+
stopListening()
|
|
69
|
+
reactApplicationContext.removeLifecycleEventListener(this)
|
|
70
|
+
executor.shutdown()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private fun attachListener() {
|
|
74
|
+
val activity = reactApplicationContext.currentActivity
|
|
75
|
+
val windowTracker = tracker
|
|
76
|
+
if (!listening || activity == null || windowTracker == null) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
windowTracker.removeWindowLayoutInfoListener(layoutCallback)
|
|
80
|
+
windowTracker.addWindowLayoutInfoListener(activity, executor, layoutCallback)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private fun detachListener() {
|
|
84
|
+
tracker?.removeWindowLayoutInfoListener(layoutCallback)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private fun publish(info: WindowLayoutInfo) {
|
|
88
|
+
val supported = hasHinge()
|
|
89
|
+
val foldingFeature = info.displayFeatures.filterIsInstance<FoldingFeature>().firstOrNull()
|
|
90
|
+
val next =
|
|
91
|
+
when {
|
|
92
|
+
!supported -> Posture(false, "unknown", false)
|
|
93
|
+
foldingFeature == null -> Posture(true, "closed", false)
|
|
94
|
+
foldingFeature.state == FoldingFeature.State.FLAT -> Posture(true, "fullyOpen", true)
|
|
95
|
+
foldingFeature.state == FoldingFeature.State.HALF_OPENED ->
|
|
96
|
+
Posture(true, "partiallyOpen", false)
|
|
97
|
+
else -> Posture(true, "unknown", false)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (next == posture) {
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
posture = next
|
|
104
|
+
val snapshot = next
|
|
105
|
+
reactApplicationContext.runOnUiQueueThread {
|
|
106
|
+
emitOnFoldStateChange(snapshot.toMap())
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private fun hasHinge(): Boolean {
|
|
111
|
+
val feature =
|
|
112
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
113
|
+
PackageManager.FEATURE_SENSOR_HINGE_ANGLE
|
|
114
|
+
} else {
|
|
115
|
+
"android.hardware.sensor.hinge_angle"
|
|
116
|
+
}
|
|
117
|
+
return reactApplicationContext.packageManager.hasSystemFeature(feature)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private fun Posture.toMap(): WritableMap {
|
|
121
|
+
return Arguments.createMap().apply {
|
|
122
|
+
putBoolean("isFoldSupported", isFoldSupported)
|
|
123
|
+
putString("status", status)
|
|
124
|
+
putBoolean("isFlat", isFlat)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
companion object {
|
|
129
|
+
const val NAME = "NativeDevicePosture"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package com.deviceposture
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.BaseReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.module.model.ReactModuleInfo
|
|
7
|
+
import com.facebook.react.module.model.ReactModuleInfoProvider
|
|
8
|
+
|
|
9
|
+
class DevicePosturePackage : BaseReactPackage() {
|
|
10
|
+
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
|
|
11
|
+
return if (name == DevicePostureModule.NAME) {
|
|
12
|
+
DevicePostureModule(reactContext)
|
|
13
|
+
} else {
|
|
14
|
+
null
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override fun getReactModuleInfoProvider(): ReactModuleInfoProvider {
|
|
19
|
+
return ReactModuleInfoProvider {
|
|
20
|
+
mapOf(
|
|
21
|
+
DevicePostureModule.NAME to ReactModuleInfo(
|
|
22
|
+
DevicePostureModule.NAME,
|
|
23
|
+
DevicePostureModule::class.java.name,
|
|
24
|
+
false,
|
|
25
|
+
true,
|
|
26
|
+
false,
|
|
27
|
+
true
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#import <RNDevicePostureSpec/RNDevicePostureSpec.h>
|
|
2
|
+
#import <UIKit/UIHinge.h>
|
|
3
|
+
#import <UIKit/UIHingeInteraction.h>
|
|
4
|
+
#import <objc/runtime.h>
|
|
5
|
+
|
|
6
|
+
@class DevicePostureModule;
|
|
7
|
+
|
|
8
|
+
static NSDictionary *DevicePostureCurrentState;
|
|
9
|
+
static __weak DevicePostureModule *DevicePostureInstance;
|
|
10
|
+
static const void *DevicePostureInteractionKey = &DevicePostureInteractionKey;
|
|
11
|
+
|
|
12
|
+
static NSDictionary *DevicePostureState(BOOL isFoldSupported, NSString *status, BOOL isFlat)
|
|
13
|
+
{
|
|
14
|
+
return @{
|
|
15
|
+
@"isFoldSupported" : @(isFoldSupported),
|
|
16
|
+
@"status" : status ?: @"unknown",
|
|
17
|
+
@"isFlat" : @(isFlat),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static UIView *DevicePostureRootView(void)
|
|
22
|
+
{
|
|
23
|
+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
|
24
|
+
if (![scene isKindOfClass:[UIWindowScene class]]) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
|
28
|
+
for (UIWindow *window in windowScene.windows) {
|
|
29
|
+
if (window.isKeyWindow && window.rootViewController.view != nil) {
|
|
30
|
+
return window.rootViewController.view;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return nil;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@interface DevicePostureModule : NativeDevicePostureSpecBase <NativeDevicePostureSpec>
|
|
38
|
+
+ (void)applySupported:(BOOL)isFoldSupported status:(NSString *)status isFlat:(BOOL)isFlat;
|
|
39
|
+
- (void)attachHingeObserver;
|
|
40
|
+
- (void)detachHingeObserver;
|
|
41
|
+
@end
|
|
42
|
+
|
|
43
|
+
@implementation DevicePostureModule
|
|
44
|
+
|
|
45
|
+
RCT_EXPORT_MODULE(NativeDevicePosture);
|
|
46
|
+
|
|
47
|
+
+ (void)initialize
|
|
48
|
+
{
|
|
49
|
+
if (self == [DevicePostureModule class] && DevicePostureCurrentState == nil) {
|
|
50
|
+
DevicePostureCurrentState = DevicePostureState(NO, @"unknown", NO);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
+ (BOOL)requiresMainQueueSetup
|
|
55
|
+
{
|
|
56
|
+
return YES;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
- (instancetype)init
|
|
60
|
+
{
|
|
61
|
+
if (self = [super init]) {
|
|
62
|
+
DevicePostureInstance = self;
|
|
63
|
+
}
|
|
64
|
+
return self;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
- (void)startListening
|
|
68
|
+
{
|
|
69
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
70
|
+
[self attachHingeObserver];
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
- (void)stopListening
|
|
75
|
+
{
|
|
76
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
77
|
+
[self detachHingeObserver];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (void)attachHingeObserver
|
|
82
|
+
{
|
|
83
|
+
if (@available(iOS 27.1, *)) {
|
|
84
|
+
UIView *view = DevicePostureRootView();
|
|
85
|
+
if (view == nil || objc_getAssociatedObject(view, DevicePostureInteractionKey) != nil) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
UIHingeInteraction *interaction = [[UIHingeInteraction alloc]
|
|
90
|
+
initWithUpdateHandler:^(__unused UIHingeInteraction *hingeInteraction,
|
|
91
|
+
UIHingeInteractionUpdate *update) {
|
|
92
|
+
UIHinge *hinge = update.hinge;
|
|
93
|
+
if (hinge == nil) {
|
|
94
|
+
[DevicePostureModule applySupported:NO status:@"unknown" isFlat:NO];
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
switch (hinge.status) {
|
|
99
|
+
case UIHingeStatusClosed:
|
|
100
|
+
[DevicePostureModule applySupported:YES status:@"closed" isFlat:NO];
|
|
101
|
+
break;
|
|
102
|
+
case UIHingeStatusPartiallyOpen:
|
|
103
|
+
[DevicePostureModule applySupported:YES status:@"partiallyOpen" isFlat:NO];
|
|
104
|
+
break;
|
|
105
|
+
case UIHingeStatusFullyOpen:
|
|
106
|
+
[DevicePostureModule applySupported:YES status:@"fullyOpen" isFlat:YES];
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
[DevicePostureModule applySupported:YES status:@"unknown" isFlat:NO];
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}];
|
|
113
|
+
|
|
114
|
+
objc_setAssociatedObject(
|
|
115
|
+
view, DevicePostureInteractionKey, interaction, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
116
|
+
[view addInteraction:interaction];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
- (void)detachHingeObserver
|
|
121
|
+
{
|
|
122
|
+
if (@available(iOS 27.1, *)) {
|
|
123
|
+
UIView *view = DevicePostureRootView();
|
|
124
|
+
id interaction = view ? objc_getAssociatedObject(view, DevicePostureInteractionKey) : nil;
|
|
125
|
+
if (view != nil && interaction != nil) {
|
|
126
|
+
[view removeInteraction:interaction];
|
|
127
|
+
objc_setAssociatedObject(view, DevicePostureInteractionKey, nil, OBJC_ASSOCIATION_ASSIGN);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
+ (void)applySupported:(BOOL)isFoldSupported status:(NSString *)status isFlat:(BOOL)isFlat
|
|
133
|
+
{
|
|
134
|
+
void (^apply)(void) = ^{
|
|
135
|
+
NSDictionary *next = DevicePostureState(isFoldSupported, status, isFlat);
|
|
136
|
+
if ([DevicePostureCurrentState isEqualToDictionary:next]) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
DevicePostureCurrentState = next;
|
|
140
|
+
DevicePostureModule *module = DevicePostureInstance;
|
|
141
|
+
if (module != nil) {
|
|
142
|
+
[module emitOnFoldStateChange:next];
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if ([NSThread isMainThread]) {
|
|
147
|
+
apply();
|
|
148
|
+
} else {
|
|
149
|
+
dispatch_async(dispatch_get_main_queue(), apply);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
- (NSDictionary *)getFoldState
|
|
154
|
+
{
|
|
155
|
+
return DevicePostureCurrentState ?: DevicePostureState(NO, @"unknown", NO);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
159
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
160
|
+
{
|
|
161
|
+
return std::make_shared<facebook::react::NativeDevicePostureSpecJSI>(params);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@end
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-device-posture",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared fold and hinge posture for iOS and Android, available through one provider.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"react-native": "src/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"src",
|
|
9
|
+
"ios",
|
|
10
|
+
"android",
|
|
11
|
+
"react-native-device-posture.podspec",
|
|
12
|
+
"react-native.config.js",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test __tests__/normalizeFoldState.test.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"react-native",
|
|
20
|
+
"fold",
|
|
21
|
+
"hinge",
|
|
22
|
+
"device-posture",
|
|
23
|
+
"iphone-duo",
|
|
24
|
+
"foldable"
|
|
25
|
+
],
|
|
26
|
+
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"react": ">=18",
|
|
30
|
+
"react-native": ">=0.76"
|
|
31
|
+
},
|
|
32
|
+
"codegenConfig": {
|
|
33
|
+
"name": "RNDevicePostureSpec",
|
|
34
|
+
"type": "modules",
|
|
35
|
+
"jsSrcsDir": "src",
|
|
36
|
+
"android": {
|
|
37
|
+
"javaPackageName": "com.deviceposture"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
|
|
5
|
+
|
|
6
|
+
Pod::Spec.new do |s|
|
|
7
|
+
s.name = "react-native-device-posture"
|
|
8
|
+
s.version = package["version"]
|
|
9
|
+
s.summary = package["description"]
|
|
10
|
+
s.homepage = "https://github.com/device-posture/react-native-device-posture"
|
|
11
|
+
s.license = "MIT"
|
|
12
|
+
s.authors = "device-posture"
|
|
13
|
+
s.platforms = { :ios => "15.1" }
|
|
14
|
+
s.source = { :git => "https://github.com/device-posture/react-native-device-posture.git", :tag => "#{s.version}" }
|
|
15
|
+
s.source_files = "ios/**/*.{h,m,mm}"
|
|
16
|
+
|
|
17
|
+
if respond_to?(:install_modules_dependencies, true)
|
|
18
|
+
install_modules_dependencies(s)
|
|
19
|
+
else
|
|
20
|
+
s.dependency "React-Core"
|
|
21
|
+
if ENV["RCT_NEW_ARCH_ENABLED"] == "1"
|
|
22
|
+
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
|
|
23
|
+
s.pod_target_xcconfig = {
|
|
24
|
+
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
|
|
25
|
+
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
|
|
26
|
+
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20"
|
|
27
|
+
}
|
|
28
|
+
s.dependency "React-Codegen"
|
|
29
|
+
s.dependency "RCT-Folly"
|
|
30
|
+
s.dependency "RCTRequired"
|
|
31
|
+
s.dependency "RCTTypeSafety"
|
|
32
|
+
s.dependency "ReactCommon/turbomodule/core"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const React = require('react');
|
|
2
|
+
const { FOLD_STATE_EVENT } = require('./constants');
|
|
3
|
+
const { EMPTY_FOLD_STATE, normalizeFoldState } = require('./normalizeFoldState');
|
|
4
|
+
|
|
5
|
+
const FoldStateContext = React.createContext(null);
|
|
6
|
+
|
|
7
|
+
function readInitialState() {
|
|
8
|
+
let NativeDevicePosture;
|
|
9
|
+
try {
|
|
10
|
+
NativeDevicePosture = require('./NativeDevicePosture').default;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
return EMPTY_FOLD_STATE;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!NativeDevicePosture || typeof NativeDevicePosture.getFoldState !== 'function') {
|
|
16
|
+
return EMPTY_FOLD_STATE;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return normalizeFoldState(NativeDevicePosture.getFoldState());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function FoldStateProvider({ children }) {
|
|
23
|
+
const [foldState, setFoldState] = React.useState(readInitialState);
|
|
24
|
+
|
|
25
|
+
React.useEffect(() => {
|
|
26
|
+
let NativeDevicePosture;
|
|
27
|
+
try {
|
|
28
|
+
NativeDevicePosture = require('./NativeDevicePosture').default;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const listen = NativeDevicePosture && NativeDevicePosture[FOLD_STATE_EVENT];
|
|
34
|
+
if (!NativeDevicePosture || typeof listen !== 'function') {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
NativeDevicePosture.startListening();
|
|
39
|
+
const subscription = listen((next) => {
|
|
40
|
+
setFoldState(normalizeFoldState(next));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return () => {
|
|
44
|
+
subscription?.remove?.();
|
|
45
|
+
NativeDevicePosture.stopListening?.();
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
return React.createElement(
|
|
50
|
+
FoldStateContext.Provider,
|
|
51
|
+
{ value: foldState },
|
|
52
|
+
children
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function useFoldState() {
|
|
57
|
+
const foldState = React.useContext(FoldStateContext);
|
|
58
|
+
if (foldState == null) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'useFoldState must be used within FoldStateProvider'
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return foldState;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
FoldStateProvider,
|
|
68
|
+
useFoldState,
|
|
69
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TurboModule } from 'react-native';
|
|
2
|
+
import { TurboModuleRegistry } from 'react-native';
|
|
3
|
+
import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
|
|
4
|
+
|
|
5
|
+
export type FoldState = {
|
|
6
|
+
isFoldSupported: boolean;
|
|
7
|
+
status: string;
|
|
8
|
+
isFlat: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export interface Spec extends TurboModule {
|
|
12
|
+
getFoldState(): FoldState;
|
|
13
|
+
startListening(): void;
|
|
14
|
+
stopListening(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Shared listener for iOS and Android. The property name is `onFoldStateChange`.
|
|
17
|
+
*/
|
|
18
|
+
readonly onFoldStateChange: EventEmitter<FoldState>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default TurboModuleRegistry.get<Spec>('NativeDevicePosture');
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same listener name on iOS and Android.
|
|
3
|
+
* Subscribe with `nativeModule[FOLD_STATE_EVENT](handler)`.
|
|
4
|
+
*/
|
|
5
|
+
const FOLD_STATE_EVENT = 'onFoldStateChange';
|
|
6
|
+
|
|
7
|
+
const FOLD_STATUS = {
|
|
8
|
+
UNKNOWN: 'unknown',
|
|
9
|
+
CLOSED: 'closed',
|
|
10
|
+
PARTIALLY_OPEN: 'partiallyOpen',
|
|
11
|
+
FULLY_OPEN: 'fullyOpen',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
FOLD_STATE_EVENT,
|
|
16
|
+
FOLD_STATUS,
|
|
17
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const { FOLD_STATE_EVENT, FOLD_STATUS } = require('./constants');
|
|
2
|
+
const { EMPTY_FOLD_STATE, normalizeFoldState } = require('./normalizeFoldState');
|
|
3
|
+
const { FoldStateProvider, useFoldState } = require('./FoldStateProvider');
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
FOLD_STATE_EVENT,
|
|
7
|
+
FOLD_STATUS,
|
|
8
|
+
EMPTY_FOLD_STATE,
|
|
9
|
+
normalizeFoldState,
|
|
10
|
+
FoldStateProvider,
|
|
11
|
+
useFoldState,
|
|
12
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { FOLD_STATUS } = require('./constants');
|
|
2
|
+
|
|
3
|
+
const KNOWN_STATUSES = new Set(Object.values(FOLD_STATUS));
|
|
4
|
+
|
|
5
|
+
const EMPTY_FOLD_STATE = {
|
|
6
|
+
isFoldSupported: false,
|
|
7
|
+
status: FOLD_STATUS.UNKNOWN,
|
|
8
|
+
isFlat: false,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function normalizeFoldState(raw) {
|
|
12
|
+
const isFoldSupported = Boolean(raw && raw.isFoldSupported);
|
|
13
|
+
const status =
|
|
14
|
+
raw && KNOWN_STATUSES.has(raw.status) ? raw.status : FOLD_STATUS.UNKNOWN;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
isFoldSupported,
|
|
18
|
+
status: isFoldSupported ? status : FOLD_STATUS.UNKNOWN,
|
|
19
|
+
isFlat: isFoldSupported && status === FOLD_STATUS.FULLY_OPEN,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
EMPTY_FOLD_STATE,
|
|
25
|
+
normalizeFoldState,
|
|
26
|
+
};
|