diff --git a/apps/brownfield-tester/android-integrated/app/build.gradle.kts b/apps/brownfield-tester/android-integrated/app/build.gradle.kts index a39bd4c540dfa9..173f47d511ff79 100644 --- a/apps/brownfield-tester/android-integrated/app/build.gradle.kts +++ b/apps/brownfield-tester/android-integrated/app/build.gradle.kts @@ -10,7 +10,7 @@ android { defaultConfig { applicationId = "dev.expo.brownfieldintegratedtester" - minSdk = 24 + minSdk = 26 targetSdk = 36 versionCode = 1 versionName = "1.0" diff --git a/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/BrownfieldTestActivity.kt b/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/BrownfieldTestActivity.kt new file mode 100644 index 00000000000000..f0e96d534ae7fd --- /dev/null +++ b/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/BrownfieldTestActivity.kt @@ -0,0 +1,106 @@ +package dev.expo.brownfieldintegratedtester + +import android.util.Log +import android.widget.Toast +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler +import expo.modules.brownfield.BrownfieldMessage +import expo.modules.brownfield.BrownfieldMessaging +import expo.modules.brownfield.BrownfieldState +import expo.modules.brownfield.Removable +import host.exp.exponent.brownfield.BrownfieldActivity +import java.util.Timer +import kotlin.concurrent.timerTask + +open class BrownfieldTestActivity : BrownfieldActivity(), DefaultHardwareBackBtnHandler { + // Listeners + private var messagingListenerId: String? = null + private var stateListener: Removable? = null + + // Other test utils + private var messageTimer: Timer? = null + private var messageCounter = 0 + + fun setupBrownfieldTests() { + // Messaging + messagingListenerId = + BrownfieldMessaging.addListener { message -> + Log.i("BrownfieldTestActivity", "Message from React Native received:") + Log.i("BrownfieldTestActivity", message.toString()) + showToast(message) + } + + // Shared state + stateListener = + BrownfieldState.subscribe("counter") { state: Any? -> + val count = state as? Double + if (count == null) { + Log.i("BrownfieldTestActivity", "Failed to parse state update as a double") + return@subscribe + } + // Return (synchronize) duplicated value to JS + BrownfieldState.set("counter-duplicated", count * 2) + } + + startMessageTimer() + } + + override fun onDestroy() { + super.onDestroy() + // Clean up messaging tests + messagingListenerId?.let { BrownfieldMessaging.removeListener(it) } + stopMessageTimer() + // Clean up state tests + stateListener?.remove() + } + + private fun startMessageTimer() { + messageTimer = + Timer().apply { + schedule( + timerTask { + sendMessage() + setTime() + }, + 0, + 1000, + ) + } + } + + private fun stopMessageTimer() { + messageTimer?.cancel() + messageTimer = null + } + + private fun showToast(message: BrownfieldMessage) { + val sender = message["sender"] as? String + val nested = message["source"] as? Map<*, *> + val platform = nested?.get("platform") as? String + if (sender != null && platform != null) { + Toast.makeText(this, "$platform($sender)", Toast.LENGTH_LONG).show() + } + } + + private fun sendMessage() { + messageCounter++ + val nativeMessage = + mapOf( + "source" to mapOf("platform" to "Android"), + "counter" to messageCounter, + "timestamp" to System.currentTimeMillis(), + "array" to listOf("ab", 'c', false, 1, 2.45), + ) + BrownfieldMessaging.sendMessage(nativeMessage) + } + + private fun setTime() { + val timeString = + java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")) + BrownfieldState.set("time", mapOf("time" to timeString)) + } + + override fun invokeDefaultOnBackPressed() { + TODO("Not yet implemented") + } +} diff --git a/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/MainActivity.kt b/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/MainActivity.kt index a033acd606e93f..212737e3224534 100644 --- a/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/MainActivity.kt +++ b/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/MainActivity.kt @@ -11,33 +11,35 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val rootLayout = LinearLayout(this).apply { - orientation = LinearLayout.VERTICAL - gravity = Gravity.CENTER - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val rootLayout = + LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + gravity = Gravity.CENTER + layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) } - val button = Button(this).apply { - text = "Open React Native app" - backgroundTintList = ContextCompat.getColorStateList(context, R.color.purple_500) - id = R.id.openReactNativeButton - setTextColor(Color.WHITE) - layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT - ) + val button = + Button(this).apply { + text = "Open React Native app" + backgroundTintList = ContextCompat.getColorStateList(context, R.color.purple_500) + id = R.id.openReactNativeButton + setTextColor(Color.WHITE) + layoutParams = + LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT, + ) - setOnClickListener { - startActivity(Intent(context, ReactNativeActivity::class.java)) - } + setOnClickListener { startActivity(Intent(context, ReactNativeActivity::class.java)) } } - rootLayout.addView(button) - setContentView(rootLayout) - } -} \ No newline at end of file + rootLayout.addView(button) + setContentView(rootLayout) + } +} diff --git a/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/ReactNativeActivity.kt b/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/ReactNativeActivity.kt index 160226c3c1396e..83af940f15fab7 100644 --- a/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/ReactNativeActivity.kt +++ b/apps/brownfield-tester/android-integrated/app/src/main/java/dev/expo/brownfieldintegratedtester/ReactNativeActivity.kt @@ -1,77 +1,14 @@ package dev.expo.brownfieldintegratedtester import android.os.Bundle -import android.widget.Toast import androidx.activity.enableEdgeToEdge -import host.exp.exponent.brownfield.BrownfieldActivity import host.exp.exponent.brownfield.showReactNativeFragment -import expo.modules.brownfield.BrownfieldMessage -import expo.modules.brownfield.BrownfieldMessaging -import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler -import java.util.Timer -import kotlin.concurrent.timerTask -class ReactNativeActivity : BrownfieldActivity(), DefaultHardwareBackBtnHandler { - private var listenerId: String? = null - private var messageTimer: Timer? = null - private var messageCounter = 0 - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - enableEdgeToEdge() - showReactNativeFragment() - - listenerId = - BrownfieldMessaging.addListener { message -> - println("Message from React Native received:") - println(message) - showToast(message) - } - startMessageTimer() - } - - override fun onDestroy() { - super.onDestroy() - listenerId?.let { BrownfieldMessaging.removeListener(it) } - stopMessageTimer() - } - - private fun startMessageTimer() { - messageTimer = Timer() - // Schedule: delay 0ms, repeat every 5000ms (5 seconds) - messageTimer?.schedule(timerTask { - sendMessage() - }, 0, 2500) - } - - private fun stopMessageTimer() { - messageTimer?.cancel() - messageTimer = null - } - - private fun showToast(message: BrownfieldMessage) { - val sender = message["sender"] as? String - val nested = message["source"] as? Map<*, *> - val platform = nested?.get("platform") as? String - if (sender != null && platform != null) { - Toast.makeText(this, "$platform($sender)", Toast.LENGTH_LONG).show() - } - } - - private fun sendMessage() { - messageCounter++ - val nativeMessage = mapOf( - "source" to mapOf( - "platform" to "Android" - ), - "counter" to messageCounter, - "timestamp" to System.currentTimeMillis(), - "array" to listOf("ab", 'c', false, 1, 2.45) - ) - BrownfieldMessaging.sendMessage(nativeMessage) - } - - override fun invokeDefaultOnBackPressed() { - TODO("Not yet implemented") - } -} \ No newline at end of file +class ReactNativeActivity : BrownfieldTestActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + showReactNativeFragment() + setupBrownfieldTests() + } +} diff --git a/apps/brownfield-tester/expo-app/src/app/apis/index.tsx b/apps/brownfield-tester/expo-app/src/app/apis/index.tsx index 96a0f744665fb1..4ccf1ae7f730d2 100644 --- a/apps/brownfield-tester/expo-app/src/app/apis/index.tsx +++ b/apps/brownfield-tester/expo-app/src/app/apis/index.tsx @@ -7,7 +7,7 @@ import { ActionButton } from '@/components'; const Index = () => { const router = useRouter(); - const navigateToScreen = (screen: 'communication' | 'navigation') => { + const navigateToScreen = (screen: 'communication' | 'navigation' | 'state') => { router.navigate(`/apis/${screen}`); }; @@ -29,6 +29,14 @@ const Index = () => { onPress={() => navigateToScreen('navigation')} testID="apis-navigation" /> + navigateToScreen('state')} + testID="apis-state" + /> ); }; diff --git a/apps/brownfield-tester/expo-app/src/app/apis/state.tsx b/apps/brownfield-tester/expo-app/src/app/apis/state.tsx new file mode 100644 index 00000000000000..d488fdb23fb409 --- /dev/null +++ b/apps/brownfield-tester/expo-app/src/app/apis/state.tsx @@ -0,0 +1,136 @@ +import Feather from '@expo/vector-icons/Feather'; +import * as ExpoBrownfield from 'expo-brownfield'; +import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { SafeAreaView } from 'react-native-safe-area-context'; + +import { Header } from '@/components'; + +const State = () => { + const [counter, setCounter] = ExpoBrownfield.useSharedState('counter', 0); + const [counterDuplicated] = ExpoBrownfield.useSharedState('counter-duplicated', 0); + const [time] = ExpoBrownfield.useSharedState('time'); + + return ( + +
+ {/* JS to native synchronization */} + JS to native synchronization + + setCounter((prev) => (prev ?? 0) + 1)}> + + + {String(counter)} + setCounter((prev) => (prev ?? 0) - 1)}> + + + + + + + Counter duplicated (in native): {String(counterDuplicated)} + + + {/* Native to JS synchronization */} + Native to JS synchronization + + Time: {time?.time ?? 'N / A'} + + + + ); +}; + +const InputDemo = () => { + const [counter] = ExpoBrownfield.useSharedState('counter'); + return ; +}; + +const BlockDemo = () => { + const [counter] = ExpoBrownfield.useSharedState('counter'); + return Counter: {String(counter)}; +}; + +const TimeInputDemo = () => { + const [time] = ExpoBrownfield.useSharedState('time'); + return ; +}; + +export default State; + +const styles = StyleSheet.create({ + counterDemoContainer: { + paddingHorizontal: 24, + gap: 16, + }, + counterInput: { + fontSize: 18, + fontWeight: 'semibold', + color: 'black', + textAlign: 'center', + borderWidth: 1, + borderRadius: 8, + borderColor: 'gray', + }, + counterBlock: { + fontSize: 18, + fontWeight: 'semibold', + color: 'black', + textAlign: 'center', + borderWidth: 1, + borderRadius: 8, + borderColor: 'orange', + }, + counterButton: { + backgroundColor: '#2563eb', + width: 56, + height: 56, + alignItems: 'center', + justifyContent: 'center', + borderRadius: 28, + }, + counterContainer: { + flexDirection: 'row', + gap: 24, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 32, + }, + counterText: { + fontSize: 64, + fontWeight: 'semibold', + color: 'black', + }, + subTitle: { + fontSize: 18, + fontWeight: 'bold', + color: 'black', + marginTop: 20, + textAlign: 'center', + }, + timeDemoContainer: { + paddingHorizontal: 24, + paddingTop: 16, + gap: 16, + }, + timeInput: { + fontSize: 18, + fontWeight: 'semibold', + color: 'black', + textAlign: 'center', + borderWidth: 1, + borderRadius: 8, + borderColor: 'gray', + }, + timeText: { + fontSize: 28, + fontWeight: 'bold', + color: 'black', + textAlign: 'center', + }, +}); diff --git a/packages/expo-brownfield/CHANGELOG.md b/packages/expo-brownfield/CHANGELOG.md index 04fbab72089924..c28786ebe608c8 100644 --- a/packages/expo-brownfield/CHANGELOG.md +++ b/packages/expo-brownfield/CHANGELOG.md @@ -6,6 +6,8 @@ ### 🎉 New features +- [android] add basic implementation of shared state for android ([#43097](https://github.com/expo/expo/pull/43097) by [@pmleczek](https://github.com/pmleczek)) + ### 🐛 Bug fixes ### 💡 Others diff --git a/packages/expo-brownfield/android/src/main/java/expo/modules/brownfield/ExpoBrownfieldStateModule.kt b/packages/expo-brownfield/android/src/main/java/expo/modules/brownfield/ExpoBrownfieldStateModule.kt new file mode 100644 index 00000000000000..1106918e6b2b65 --- /dev/null +++ b/packages/expo-brownfield/android/src/main/java/expo/modules/brownfield/ExpoBrownfieldStateModule.kt @@ -0,0 +1,26 @@ +package expo.modules.brownfield + +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class ExpoBrownfieldStateModule : Module() { + override fun definition() = ModuleDefinition { + Name("ExpoBrownfieldStateModule") + + Class(SharedState::class) { + Constructor { SharedState() } + + Function("get") { state: SharedState -> + return@Function state.get() + } + + Function("set") { state: SharedState, value: Any? -> state.set(value) } + } + + Function("getSharedState") { key: String -> + return@Function BrownfieldState.getOrCreate(key) + } + + Function("deleteSharedState") { key: String -> BrownfieldState.delete(key) } + } +} diff --git a/packages/expo-brownfield/android/src/main/java/expo/modules/brownfield/SharedState.kt b/packages/expo-brownfield/android/src/main/java/expo/modules/brownfield/SharedState.kt new file mode 100644 index 00000000000000..aecb59c27c3840 --- /dev/null +++ b/packages/expo-brownfield/android/src/main/java/expo/modules/brownfield/SharedState.kt @@ -0,0 +1,39 @@ +package expo.modules.brownfield + +import expo.modules.kotlin.sharedobjects.SharedObject + +fun interface Removable { + fun remove() +} + +class SharedState : SharedObject() { + private var value: Any? = null + private val listeners = mutableListOf<(Any?) -> Unit>() + + fun get(): Any? { + synchronized(this) { + return value + } + } + + fun set(newValue: Any?) { + val listenersSnapshot: List<(Any?) -> Unit> + synchronized(this) { + value = newValue + listenersSnapshot = listeners.toList() + } + emit("change", mapOf("value" to newValue)) + listenersSnapshot.forEach { it(newValue) } + } + + fun addListener(listener: ((Any?) -> Unit)): Removable { + synchronized(this) { + listeners.add(listener) + } + return Removable { + synchronized(this) { + listeners.remove(listener) + } + } + } +} diff --git a/packages/expo-brownfield/android/src/main/java/expo/modules/brownfield/State.kt b/packages/expo-brownfield/android/src/main/java/expo/modules/brownfield/State.kt new file mode 100644 index 00000000000000..70af02067fe108 --- /dev/null +++ b/packages/expo-brownfield/android/src/main/java/expo/modules/brownfield/State.kt @@ -0,0 +1,39 @@ +package expo.modules.brownfield + +object BrownfieldState { + private val registry = mutableMapOf() + + fun getOrCreate(key: String): SharedState { + synchronized(this) { + return registry.getOrPut(key) { SharedState() } + } + } + + fun get(key: String): Any? { + synchronized(this) { + return registry[key]?.get() + } + } + + fun set(key: String, value: Any?) { + val state: SharedState + synchronized(this) { + state = registry.getOrPut(key) { SharedState() } + } + state.set(value) + } + + fun subscribe(key: String, callback: (Any?) -> Unit): Removable { + val state: SharedState + synchronized(this) { + state = registry.getOrPut(key) { SharedState() } + } + return state.addListener(callback) + } + + fun delete(key: String): Any? { + synchronized(this) { + return registry.remove(key)?.get() + } + } +} diff --git a/packages/expo-brownfield/build/ExpoBrownfieldModule.d.ts b/packages/expo-brownfield/build/ExpoBrownfieldModule.d.ts index 9eb4b73e4db742..9bd6e8c3986f3a 100644 --- a/packages/expo-brownfield/build/ExpoBrownfieldModule.d.ts +++ b/packages/expo-brownfield/build/ExpoBrownfieldModule.d.ts @@ -1,4 +1,60 @@ -import type { ExpoBrownfieldModuleSpec } from './types'; -declare const _default: ExpoBrownfieldModuleSpec; -export default _default; +import type { EventSubscription } from 'expo-modules-core'; +import type { Listener, MessageEvent } from './ExpoBrownfieldModule.types'; +export { EventSubscription }; +export type { MessageEvent }; +/** + * Navigates back to the native part of the app, dismissing the React Native view. + * + * @param animated Whether to animate the transition (iOS only). Defaults to `false`. + * @default false + */ +export declare function popToNative(animated?: boolean): void; +/** + * Enables or disables the native back button behavior. When enabled, pressing the + * back button will navigate back to the native part of the app instead of + * performing the default React Navigation back action. + * + * @param enabled Whether to enable native back button handling. + */ +export declare function setNativeBackEnabled(enabled: boolean): void; +/** + * Adds a listener for messages sent from the native side of the app. + * + * @param listener A callback function that receives message events from native. + * @returns A subscription object that can be used to remove the listener. + * + * @example + * ```ts + * const subscription = addMessageListener((event) => { + * console.log('Received message from native:', event); + * }); + * + * // Later, to remove the listener: + * subscription.remove(); + * ``` + */ +export declare function addMessageListener(listener: Listener): EventSubscription; +/** + * Sends a message to the native side of the app. The message can be received by + * setting up a listener in the native code. + * + * @param message A dictionary containing the message payload to send to native. + */ +export declare function sendMessage(message: Record): void; +/** + * Removes a specific message listener. + * + * @param listener The listener function to remove. + */ +export declare function removeMessageListener(listener: Listener): void; +/** + * Removes all message listeners. + */ +export declare function removeAllMessageListeners(): void; +/** + * Gets the number of registered message listeners. + * + * @returns The number of active message listeners. + */ +export declare function getMessageListenerCount(): number; //# sourceMappingURL=ExpoBrownfieldModule.d.ts.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldModule.d.ts.map b/packages/expo-brownfield/build/ExpoBrownfieldModule.d.ts.map index ad57d5ac6c4ec7..07f69e276f732d 100644 --- a/packages/expo-brownfield/build/ExpoBrownfieldModule.d.ts.map +++ b/packages/expo-brownfield/build/ExpoBrownfieldModule.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"ExpoBrownfieldModule.d.ts","sourceRoot":"","sources":["../src/ExpoBrownfieldModule.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,SAAS,CAAC;;AAExD,wBAAqF"} \ No newline at end of file +{"version":3,"file":"ExpoBrownfieldModule.d.ts","sourceRoot":"","sources":["../src/ExpoBrownfieldModule.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,OAAO,KAAK,EAEV,QAAQ,EACR,YAAY,EACb,MAAM,8BAA8B,CAAC;AAItC,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,YAAY,EAAE,YAAY,EAAE,CAAC;AAI7B;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,GAAE,OAAe,GAAG,IAAI,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE3D;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,iBAAiB,CAEtF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,IAAI,CAE5E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD"} \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldModule.js b/packages/expo-brownfield/build/ExpoBrownfieldModule.js index 999f50fe68d3a1..47c8f10ef7279c 100644 --- a/packages/expo-brownfield/build/ExpoBrownfieldModule.js +++ b/packages/expo-brownfield/build/ExpoBrownfieldModule.js @@ -1,3 +1,76 @@ import { requireNativeModule } from 'expo'; -export default requireNativeModule('ExpoBrownfieldModule'); +const ExpoBrownfieldModule = requireNativeModule('ExpoBrownfieldModule'); +// SECTION: Navigation API +/** + * Navigates back to the native part of the app, dismissing the React Native view. + * + * @param animated Whether to animate the transition (iOS only). Defaults to `false`. + * @default false + */ +export function popToNative(animated = false) { + ExpoBrownfieldModule.popToNative(animated); +} +/** + * Enables or disables the native back button behavior. When enabled, pressing the + * back button will navigate back to the native part of the app instead of + * performing the default React Navigation back action. + * + * @param enabled Whether to enable native back button handling. + */ +export function setNativeBackEnabled(enabled) { + ExpoBrownfieldModule.setNativeBackEnabled(enabled); +} +// END SECTION: Navigation API +// SECTION: Messaging API +/** + * Adds a listener for messages sent from the native side of the app. + * + * @param listener A callback function that receives message events from native. + * @returns A subscription object that can be used to remove the listener. + * + * @example + * ```ts + * const subscription = addMessageListener((event) => { + * console.log('Received message from native:', event); + * }); + * + * // Later, to remove the listener: + * subscription.remove(); + * ``` + */ +export function addMessageListener(listener) { + return ExpoBrownfieldModule.addListener('onMessage', listener); +} +/** + * Sends a message to the native side of the app. The message can be received by + * setting up a listener in the native code. + * + * @param message A dictionary containing the message payload to send to native. + */ +export function sendMessage(message) { + ExpoBrownfieldModule.sendMessage(message); +} +/** + * Removes a specific message listener. + * + * @param listener The listener function to remove. + */ +export function removeMessageListener(listener) { + ExpoBrownfieldModule.removeListener('onMessage', listener); +} +/** + * Removes all message listeners. + */ +export function removeAllMessageListeners() { + ExpoBrownfieldModule.removeAllListeners('onMessage'); +} +/** + * Gets the number of registered message listeners. + * + * @returns The number of active message listeners. + */ +export function getMessageListenerCount() { + return ExpoBrownfieldModule.listenerCount('onMessage'); +} +// END SECTION: Messaging API //# sourceMappingURL=ExpoBrownfieldModule.js.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldModule.js.map b/packages/expo-brownfield/build/ExpoBrownfieldModule.js.map index 1f48c234f443d6..55db482fa40843 100644 --- a/packages/expo-brownfield/build/ExpoBrownfieldModule.js.map +++ b/packages/expo-brownfield/build/ExpoBrownfieldModule.js.map @@ -1 +1 @@ -{"version":3,"file":"ExpoBrownfieldModule.js","sourceRoot":"","sources":["../src/ExpoBrownfieldModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAI3C,eAAe,mBAAmB,CAA2B,sBAAsB,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from 'expo';\n\nimport type { ExpoBrownfieldModuleSpec } from './types';\n\nexport default requireNativeModule('ExpoBrownfieldModule');\n"]} \ No newline at end of file +{"version":3,"file":"ExpoBrownfieldModule.js","sourceRoot":"","sources":["../src/ExpoBrownfieldModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAS3C,MAAM,oBAAoB,GAAG,mBAAmB,CAA2B,sBAAsB,CAAC,CAAC;AAKnG,0BAA0B;AAE1B;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,WAAoB,KAAK;IACnD,oBAAoB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,oBAAoB,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;AACrD,CAAC;AAED,8BAA8B;AAE9B,yBAAyB;AAEzB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgC;IACjE,OAAO,oBAAoB,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AACjE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,OAA4B;IACtD,oBAAoB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgC;IACpE,oBAAoB,CAAC,cAAc,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB;IACvC,oBAAoB,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,oBAAoB,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AACzD,CAAC;AAED,6BAA6B","sourcesContent":["import { requireNativeModule } from 'expo';\nimport type { EventSubscription } from 'expo-modules-core';\n\nimport type {\n ExpoBrownfieldModuleSpec,\n Listener,\n MessageEvent,\n} from './ExpoBrownfieldModule.types';\n\nconst ExpoBrownfieldModule = requireNativeModule('ExpoBrownfieldModule');\n\nexport { EventSubscription };\nexport type { MessageEvent };\n\n// SECTION: Navigation API\n\n/**\n * Navigates back to the native part of the app, dismissing the React Native view.\n *\n * @param animated Whether to animate the transition (iOS only). Defaults to `false`.\n * @default false\n */\nexport function popToNative(animated: boolean = false): void {\n ExpoBrownfieldModule.popToNative(animated);\n}\n\n/**\n * Enables or disables the native back button behavior. When enabled, pressing the\n * back button will navigate back to the native part of the app instead of\n * performing the default React Navigation back action.\n *\n * @param enabled Whether to enable native back button handling.\n */\nexport function setNativeBackEnabled(enabled: boolean): void {\n ExpoBrownfieldModule.setNativeBackEnabled(enabled);\n}\n\n// END SECTION: Navigation API\n\n// SECTION: Messaging API\n\n/**\n * Adds a listener for messages sent from the native side of the app.\n *\n * @param listener A callback function that receives message events from native.\n * @returns A subscription object that can be used to remove the listener.\n *\n * @example\n * ```ts\n * const subscription = addMessageListener((event) => {\n * console.log('Received message from native:', event);\n * });\n *\n * // Later, to remove the listener:\n * subscription.remove();\n * ```\n */\nexport function addMessageListener(listener: Listener): EventSubscription {\n return ExpoBrownfieldModule.addListener('onMessage', listener);\n}\n\n/**\n * Sends a message to the native side of the app. The message can be received by\n * setting up a listener in the native code.\n *\n * @param message A dictionary containing the message payload to send to native.\n */\nexport function sendMessage(message: Record): void {\n ExpoBrownfieldModule.sendMessage(message);\n}\n\n/**\n * Removes a specific message listener.\n *\n * @param listener The listener function to remove.\n */\nexport function removeMessageListener(listener: Listener): void {\n ExpoBrownfieldModule.removeListener('onMessage', listener);\n}\n\n/**\n * Removes all message listeners.\n */\nexport function removeAllMessageListeners(): void {\n ExpoBrownfieldModule.removeAllListeners('onMessage');\n}\n\n/**\n * Gets the number of registered message listeners.\n *\n * @returns The number of active message listeners.\n */\nexport function getMessageListenerCount(): number {\n return ExpoBrownfieldModule.listenerCount('onMessage');\n}\n\n// END SECTION: Messaging API\n"]} \ No newline at end of file diff --git a/packages/expo-brownfield/build/types.d.ts b/packages/expo-brownfield/build/ExpoBrownfieldModule.types.d.ts similarity index 78% rename from packages/expo-brownfield/build/types.d.ts rename to packages/expo-brownfield/build/ExpoBrownfieldModule.types.d.ts index 8ef5e7581c7b83..a9119858be3ae2 100644 --- a/packages/expo-brownfield/build/types.d.ts +++ b/packages/expo-brownfield/build/ExpoBrownfieldModule.types.d.ts @@ -1,12 +1,12 @@ import type { NativeModule } from 'expo'; export type MessageEvent = Record; export type Listener = (event: E) => void; -export type ExpoBrownfieldModuleEvents = { +export type Events = { onMessage: (event: MessageEvent) => void; }; -export declare class ExpoBrownfieldModuleSpec extends NativeModule { +export declare class ExpoBrownfieldModuleSpec extends NativeModule { popToNative(animated: boolean): void; setNativeBackEnabled(enabled: boolean): void; sendMessage(message: Record): void; } -//# sourceMappingURL=types.d.ts.map \ No newline at end of file +//# sourceMappingURL=ExpoBrownfieldModule.types.d.ts.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldModule.types.d.ts.map b/packages/expo-brownfield/build/ExpoBrownfieldModule.types.d.ts.map new file mode 100644 index 00000000000000..4426fdbfe5bff0 --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldModule.types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ExpoBrownfieldModule.types.d.ts","sourceRoot":"","sources":["../src/ExpoBrownfieldModule.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE/C,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;AAE7C,MAAM,MAAM,MAAM,GAAG;IACnB,SAAS,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC1C,CAAC;AAEF,MAAM,CAAC,OAAO,OAAO,wBAAyB,SAAQ,YAAY,CAAC,MAAM,CAAC;IACxE,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IACpC,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAC5C,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;CAChD"} \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldModule.types.js b/packages/expo-brownfield/build/ExpoBrownfieldModule.types.js new file mode 100644 index 00000000000000..c1f7480b0af009 --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldModule.types.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=ExpoBrownfieldModule.types.js.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldModule.types.js.map b/packages/expo-brownfield/build/ExpoBrownfieldModule.types.js.map new file mode 100644 index 00000000000000..4f35e06734b223 --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldModule.types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ExpoBrownfieldModule.types.js","sourceRoot":"","sources":["../src/ExpoBrownfieldModule.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { NativeModule } from 'expo';\n\nexport type MessageEvent = Record;\n\nexport type Listener = (event: E) => void;\n\nexport type Events = {\n onMessage: (event: MessageEvent) => void;\n};\n\nexport declare class ExpoBrownfieldModuleSpec extends NativeModule {\n popToNative(animated: boolean): void;\n setNativeBackEnabled(enabled: boolean): void;\n sendMessage(message: Record): void;\n}\n"]} \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldStateModule.d.ts b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.d.ts new file mode 100644 index 00000000000000..ede35da8b6abe0 --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.d.ts @@ -0,0 +1,38 @@ +import type { EventSubscription } from 'expo-modules-core'; +/** + * Gets the value of shared state for a given key. + * + * @param key The key to get the value for. + */ +export declare function getSharedStateValue(key: string): T | undefined; +/** + * Sets the value of shared state for a given key. + * + * @param key The key to set the value for. + * @param value The value to be set. + */ +export declare function setSharedStateValue(key: string, value: T): void; +/** + * Deletes the shared state for a given key. + * + * @param key The key to delete the shared state for. + */ +export declare function deleteSharedState(key: string): void; +/** + * Adds a listener for changes to the shared state for a given key. + * + * @param key The key to add the listener for. + * @param callback The callback to be called when the shared state changes. + * @returns A subscription object that can be used to remove the listener. + */ +export declare function addSharedStateListener(key: string, callback: (value: T | undefined) => void): EventSubscription; +/** + * Hook to observe and set the value of shared state for a given key. + * Provides a synchronous API similar to `useState`. + * + * @param key The key to get the value for. + * @param initialValue The initial value to be used if the shared state is not set. + * @returns A tuple containing the value and a function to set the value. + */ +export declare function useSharedState(key: string, initialValue?: T): [T | undefined, (value: T | ((prev: T | undefined) => T)) => void]; +//# sourceMappingURL=ExpoBrownfieldStateModule.d.ts.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldStateModule.d.ts.map b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.d.ts.map new file mode 100644 index 00000000000000..32f2bf99cd3409 --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ExpoBrownfieldStateModule.d.ts","sourceRoot":"","sources":["../src/ExpoBrownfieldStateModule.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAoB3D;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAIvE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAGxE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAGnD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,GAAG,GAAG,EAC5C,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,SAAS,KAAK,IAAI,GACvC,iBAAiB,CAUnB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,CAAC,GAAG,GAAG,EACpC,GAAG,EAAE,MAAM,EACX,YAAY,CAAC,EAAE,CAAC,GACf,CAAC,CAAC,GAAG,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,CAAC,KAAK,IAAI,CAAC,CAmCpE"} \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldStateModule.js b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.js new file mode 100644 index 00000000000000..51748b596e060f --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.js @@ -0,0 +1,91 @@ +import { requireNativeModule } from 'expo'; +import { useEffect, useState } from 'react'; +const ExpoBrownfieldStateModule = requireNativeModule('ExpoBrownfieldStateModule'); +const sharedObjectCache = new Map(); +// SECTION: Shared State API +function getSharedObject(key) { + if (!sharedObjectCache.has(key)) { + sharedObjectCache.set(key, ExpoBrownfieldStateModule.getSharedState(key)); + } + return sharedObjectCache.get(key); +} +/** + * Gets the value of shared state for a given key. + * + * @param key The key to get the value for. + */ +export function getSharedStateValue(key) { + const state = getSharedObject(key); + const value = state?.get(); + return value === null ? undefined : value; +} +/** + * Sets the value of shared state for a given key. + * + * @param key The key to set the value for. + * @param value The value to be set. + */ +export function setSharedStateValue(key, value) { + const state = getSharedObject(key); + state.set(value); +} +/** + * Deletes the shared state for a given key. + * + * @param key The key to delete the shared state for. + */ +export function deleteSharedState(key) { + ExpoBrownfieldStateModule.deleteSharedState(key); + sharedObjectCache.delete(key); +} +/** + * Adds a listener for changes to the shared state for a given key. + * + * @param key The key to add the listener for. + * @param callback The callback to be called when the shared state changes. + * @returns A subscription object that can be used to remove the listener. + */ +export function addSharedStateListener(key, callback) { + const state = getSharedObject(key); + const subscription = state.addListener('change', (event) => { + callback(event); + }); + return { + remove: () => subscription.remove(), + }; +} +/** + * Hook to observe and set the value of shared state for a given key. + * Provides a synchronous API similar to `useState`. + * + * @param key The key to get the value for. + * @param initialValue The initial value to be used if the shared state is not set. + * @returns A tuple containing the value and a function to set the value. + */ +export function useSharedState(key, initialValue) { + const state = getSharedObject(key); + const [value, setValue] = useState(() => { + const currentValue = state.get(); + if (currentValue === null || currentValue === undefined) { + if (initialValue !== undefined) { + state.set(initialValue); + return initialValue; + } + return undefined; + } + return currentValue; + }); + useEffect(() => { + const subscription = state.addListener('change', (event) => { + setValue(event?.['value']); + }); + return () => subscription.remove(); + }, [state]); + const setSharedValue = (newValue) => { + const valueToSet = typeof newValue === 'function' ? newValue(value) : newValue; + state.set(valueToSet); + }; + return [value, setSharedValue]; +} +// END SECTION: Shared State API +//# sourceMappingURL=ExpoBrownfieldStateModule.js.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldStateModule.js.map b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.js.map new file mode 100644 index 00000000000000..b60d005206352d --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ExpoBrownfieldStateModule.js","sourceRoot":"","sources":["../src/ExpoBrownfieldStateModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAE3C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAI5C,MAAM,yBAAyB,GAAG,mBAAmB,CACnD,2BAA2B,CAC5B,CAAC;AAEF,MAAM,iBAAiB,GAAG,IAAI,GAAG,EAAe,CAAC;AAEjD,4BAA4B;AAE5B,SAAS,eAAe,CAAC,GAAW;IAClC,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QAChC,iBAAiB,CAAC,GAAG,CAAC,GAAG,EAAE,yBAAyB,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,iBAAiB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAU,GAAW;IACtD,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACnC,MAAM,KAAK,GAAG,KAAK,EAAE,GAAG,EAAE,CAAC;IAC3B,OAAO,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,KAAW,CAAC;AACnD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAU,GAAW,EAAE,KAAQ;IAChE,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACnC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,yBAAyB,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC;IACjD,iBAAiB,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CACpC,GAAW,EACX,QAAwC;IAExC,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IAEnC,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAoB,EAAE,EAAE;QACxE,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,MAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE;KACpC,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAC5B,GAAW,EACX,YAAgB;IAEhB,MAAM,KAAK,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IAEnC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAgB,GAAG,EAAE;QACrD,MAAM,YAAY,GAAG,KAAK,CAAC,GAAG,EAAE,CAAC;QACjC,IAAI,YAAY,KAAK,IAAI,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YACxD,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;gBAC/B,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBACxB,OAAO,YAAY,CAAC;YACtB,CAAC;YAED,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,OAAO,YAAiB,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,YAAY,GAAG,KAAK,CAAC,WAAW,CACpC,QAAQ,EACR,CAAC,KAAgD,EAAE,EAAE;YACnD,QAAQ,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7B,CAAC,CACF,CAAC;QAEF,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,CAAC;IACrC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;IAEZ,MAAM,cAAc,GAAG,CAAC,QAA0C,EAAE,EAAE;QACpE,MAAM,UAAU,GACd,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,CAAE,QAAuC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QAC9F,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACxB,CAAC,CAAC;IAEF,OAAO,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC;AACjC,CAAC;AAED,gCAAgC","sourcesContent":["import { requireNativeModule } from 'expo';\nimport type { EventSubscription } from 'expo-modules-core';\nimport { useEffect, useState } from 'react';\n\nimport type { ExpoBrownfieldStateModuleSpec } from './ExpoBrownfieldStateModule.types';\n\nconst ExpoBrownfieldStateModule = requireNativeModule(\n 'ExpoBrownfieldStateModule'\n);\n\nconst sharedObjectCache = new Map();\n\n// SECTION: Shared State API\n\nfunction getSharedObject(key: string): any {\n if (!sharedObjectCache.has(key)) {\n sharedObjectCache.set(key, ExpoBrownfieldStateModule.getSharedState(key));\n }\n return sharedObjectCache.get(key);\n}\n\n/**\n * Gets the value of shared state for a given key.\n *\n * @param key The key to get the value for.\n */\nexport function getSharedStateValue(key: string): T | undefined {\n const state = getSharedObject(key);\n const value = state?.get();\n return value === null ? undefined : (value as T);\n}\n\n/**\n * Sets the value of shared state for a given key.\n *\n * @param key The key to set the value for.\n * @param value The value to be set.\n */\nexport function setSharedStateValue(key: string, value: T): void {\n const state = getSharedObject(key);\n state.set(value);\n}\n\n/**\n * Deletes the shared state for a given key.\n *\n * @param key The key to delete the shared state for.\n */\nexport function deleteSharedState(key: string): void {\n ExpoBrownfieldStateModule.deleteSharedState(key);\n sharedObjectCache.delete(key);\n}\n\n/**\n * Adds a listener for changes to the shared state for a given key.\n *\n * @param key The key to add the listener for.\n * @param callback The callback to be called when the shared state changes.\n * @returns A subscription object that can be used to remove the listener.\n */\nexport function addSharedStateListener(\n key: string,\n callback: (value: T | undefined) => void\n): EventSubscription {\n const state = getSharedObject(key);\n\n const subscription = state.addListener('change', (event: T | undefined) => {\n callback(event);\n });\n\n return {\n remove: () => subscription.remove(),\n };\n}\n\n/**\n * Hook to observe and set the value of shared state for a given key.\n * Provides a synchronous API similar to `useState`.\n *\n * @param key The key to get the value for.\n * @param initialValue The initial value to be used if the shared state is not set.\n * @returns A tuple containing the value and a function to set the value.\n */\nexport function useSharedState(\n key: string,\n initialValue?: T\n): [T | undefined, (value: T | ((prev: T | undefined) => T)) => void] {\n const state = getSharedObject(key);\n\n const [value, setValue] = useState(() => {\n const currentValue = state.get();\n if (currentValue === null || currentValue === undefined) {\n if (initialValue !== undefined) {\n state.set(initialValue);\n return initialValue;\n }\n\n return undefined;\n }\n\n return currentValue as T;\n });\n\n useEffect(() => {\n const subscription = state.addListener(\n 'change',\n (event: Record | undefined) => {\n setValue(event?.['value']);\n }\n );\n\n return () => subscription.remove();\n }, [state]);\n\n const setSharedValue = (newValue: T | ((prev: T | undefined) => T)) => {\n const valueToSet =\n typeof newValue === 'function' ? (newValue as (prev: T | undefined) => T)(value) : newValue;\n state.set(valueToSet);\n };\n\n return [value, setSharedValue];\n}\n\n// END SECTION: Shared State API\n"]} \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.d.ts b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.d.ts new file mode 100644 index 00000000000000..555dda53038102 --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.d.ts @@ -0,0 +1,6 @@ +import type { NativeModule } from 'expo'; +export declare class ExpoBrownfieldStateModuleSpec extends NativeModule { + getSharedState(key: string): any; + deleteSharedState(key: string): void; +} +//# sourceMappingURL=ExpoBrownfieldStateModule.types.d.ts.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.d.ts.map b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.d.ts.map new file mode 100644 index 00000000000000..4c9e71c50eb548 --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"ExpoBrownfieldStateModule.types.d.ts","sourceRoot":"","sources":["../src/ExpoBrownfieldStateModule.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,MAAM,CAAC,OAAO,OAAO,6BAA8B,SAAQ,YAAY;IACrE,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG;IAChC,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CACrC"} \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.js b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.js new file mode 100644 index 00000000000000..443052555855f6 --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.js @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=ExpoBrownfieldStateModule.types.js.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.js.map b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.js.map new file mode 100644 index 00000000000000..283c5e67b4f065 --- /dev/null +++ b/packages/expo-brownfield/build/ExpoBrownfieldStateModule.types.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ExpoBrownfieldStateModule.types.js","sourceRoot":"","sources":["../src/ExpoBrownfieldStateModule.types.ts"],"names":[],"mappings":"","sourcesContent":["import type { NativeModule } from 'expo';\n\nexport declare class ExpoBrownfieldStateModuleSpec extends NativeModule {\n getSharedState(key: string): any;\n deleteSharedState(key: string): void;\n}\n"]} \ No newline at end of file diff --git a/packages/expo-brownfield/build/index.d.ts b/packages/expo-brownfield/build/index.d.ts index 2ac27acefb2c36..de3b5e82200106 100644 --- a/packages/expo-brownfield/build/index.d.ts +++ b/packages/expo-brownfield/build/index.d.ts @@ -1,60 +1,3 @@ -import type { EventSubscription } from 'expo-modules-core'; -import type { Listener, MessageEvent } from './types'; -export { EventSubscription }; -export type { MessageEvent }; -/** - * Navigates back to the native part of the app, dismissing the React Native view. - * - * @param animated Whether to animate the transition (iOS only). Defaults to `false`. - * @default false - */ -export declare function popToNative(animated?: boolean): void; -/** - * Enables or disables the native back button behavior. When enabled, pressing the - * back button will navigate back to the native part of the app instead of - * performing the default React Navigation back action. - * - * @param enabled Whether to enable native back button handling. - */ -export declare function setNativeBackEnabled(enabled: boolean): void; -/** - * Adds a listener for messages sent from the native side of the app. - * - * @param listener A callback function that receives message events from native. - * @returns A subscription object that can be used to remove the listener. - * - * @example - * ```ts - * const subscription = addMessageListener((event) => { - * console.log('Received message from native:', event); - * }); - * - * // Later, to remove the listener: - * subscription.remove(); - * ``` - */ -export declare function addMessageListener(listener: Listener): EventSubscription; -/** - * Sends a message to the native side of the app. The message can be received by - * setting up a listener in the native code. - * - * @param message A dictionary containing the message payload to send to native. - */ -export declare function sendMessage(message: Record): void; -/** - * Removes a specific message listener. - * - * @param listener The listener function to remove. - */ -export declare function removeMessageListener(listener: Listener): void; -/** - * Removes all message listeners. - */ -export declare function removeAllMessageListeners(): void; -/** - * Gets the number of registered message listeners. - * - * @returns The number of active message listeners. - */ -export declare function getMessageListenerCount(): number; +export * from './ExpoBrownfieldModule'; +export * from './ExpoBrownfieldStateModule'; //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/index.d.ts.map b/packages/expo-brownfield/build/index.d.ts.map index 9669b8a85226c7..708929d75dc5ce 100644 --- a/packages/expo-brownfield/build/index.d.ts.map +++ b/packages/expo-brownfield/build/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEtD,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,YAAY,EAAE,YAAY,EAAE,CAAC;AAE7B;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,GAAE,OAAe,GAAG,IAAI,CAE3D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAE3D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,iBAAiB,CAEtF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAE9D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,GAAG,IAAI,CAE5E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAEhD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC"} \ No newline at end of file diff --git a/packages/expo-brownfield/build/index.js b/packages/expo-brownfield/build/index.js index df86a5771f57b4..b5adc1c2f79cb4 100644 --- a/packages/expo-brownfield/build/index.js +++ b/packages/expo-brownfield/build/index.js @@ -1,71 +1,3 @@ -import ExpoBrownfieldModule from './ExpoBrownfieldModule'; -/** - * Navigates back to the native part of the app, dismissing the React Native view. - * - * @param animated Whether to animate the transition (iOS only). Defaults to `false`. - * @default false - */ -export function popToNative(animated = false) { - ExpoBrownfieldModule.popToNative(animated); -} -/** - * Enables or disables the native back button behavior. When enabled, pressing the - * back button will navigate back to the native part of the app instead of - * performing the default React Navigation back action. - * - * @param enabled Whether to enable native back button handling. - */ -export function setNativeBackEnabled(enabled) { - ExpoBrownfieldModule.setNativeBackEnabled(enabled); -} -/** - * Adds a listener for messages sent from the native side of the app. - * - * @param listener A callback function that receives message events from native. - * @returns A subscription object that can be used to remove the listener. - * - * @example - * ```ts - * const subscription = addMessageListener((event) => { - * console.log('Received message from native:', event); - * }); - * - * // Later, to remove the listener: - * subscription.remove(); - * ``` - */ -export function addMessageListener(listener) { - return ExpoBrownfieldModule.addListener('onMessage', listener); -} -/** - * Sends a message to the native side of the app. The message can be received by - * setting up a listener in the native code. - * - * @param message A dictionary containing the message payload to send to native. - */ -export function sendMessage(message) { - ExpoBrownfieldModule.sendMessage(message); -} -/** - * Removes a specific message listener. - * - * @param listener The listener function to remove. - */ -export function removeMessageListener(listener) { - ExpoBrownfieldModule.removeListener('onMessage', listener); -} -/** - * Removes all message listeners. - */ -export function removeAllMessageListeners() { - ExpoBrownfieldModule.removeAllListeners('onMessage'); -} -/** - * Gets the number of registered message listeners. - * - * @returns The number of active message listeners. - */ -export function getMessageListenerCount() { - return ExpoBrownfieldModule.listenerCount('onMessage'); -} +export * from './ExpoBrownfieldModule'; +export * from './ExpoBrownfieldStateModule'; //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/index.js.map b/packages/expo-brownfield/build/index.js.map index 8a76ef6c2da619..c7669be74e4f58 100644 --- a/packages/expo-brownfield/build/index.js.map +++ b/packages/expo-brownfield/build/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,oBAAoB,MAAM,wBAAwB,CAAC;AAM1D;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,WAAoB,KAAK;IACnD,oBAAoB,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAgB;IACnD,oBAAoB,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgC;IACjE,OAAO,oBAAoB,CAAC,WAAW,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AACjE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,OAA4B;IACtD,oBAAoB,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,QAAgC;IACpE,oBAAoB,CAAC,cAAc,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,yBAAyB;IACvC,oBAAoB,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,oBAAoB,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;AACzD,CAAC","sourcesContent":["import type { EventSubscription } from 'expo-modules-core';\n\nimport ExpoBrownfieldModule from './ExpoBrownfieldModule';\nimport type { Listener, MessageEvent } from './types';\n\nexport { EventSubscription };\nexport type { MessageEvent };\n\n/**\n * Navigates back to the native part of the app, dismissing the React Native view.\n *\n * @param animated Whether to animate the transition (iOS only). Defaults to `false`.\n * @default false\n */\nexport function popToNative(animated: boolean = false): void {\n ExpoBrownfieldModule.popToNative(animated);\n}\n\n/**\n * Enables or disables the native back button behavior. When enabled, pressing the\n * back button will navigate back to the native part of the app instead of\n * performing the default React Navigation back action.\n *\n * @param enabled Whether to enable native back button handling.\n */\nexport function setNativeBackEnabled(enabled: boolean): void {\n ExpoBrownfieldModule.setNativeBackEnabled(enabled);\n}\n\n/**\n * Adds a listener for messages sent from the native side of the app.\n *\n * @param listener A callback function that receives message events from native.\n * @returns A subscription object that can be used to remove the listener.\n *\n * @example\n * ```ts\n * const subscription = addMessageListener((event) => {\n * console.log('Received message from native:', event);\n * });\n *\n * // Later, to remove the listener:\n * subscription.remove();\n * ```\n */\nexport function addMessageListener(listener: Listener): EventSubscription {\n return ExpoBrownfieldModule.addListener('onMessage', listener);\n}\n\n/**\n * Sends a message to the native side of the app. The message can be received by\n * setting up a listener in the native code.\n *\n * @param message A dictionary containing the message payload to send to native.\n */\nexport function sendMessage(message: Record): void {\n ExpoBrownfieldModule.sendMessage(message);\n}\n\n/**\n * Removes a specific message listener.\n *\n * @param listener The listener function to remove.\n */\nexport function removeMessageListener(listener: Listener): void {\n ExpoBrownfieldModule.removeListener('onMessage', listener);\n}\n\n/**\n * Removes all message listeners.\n */\nexport function removeAllMessageListeners(): void {\n ExpoBrownfieldModule.removeAllListeners('onMessage');\n}\n\n/**\n * Gets the number of registered message listeners.\n *\n * @returns The number of active message listeners.\n */\nexport function getMessageListenerCount(): number {\n return ExpoBrownfieldModule.listenerCount('onMessage');\n}\n"]} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,wBAAwB,CAAC;AACvC,cAAc,6BAA6B,CAAC","sourcesContent":["export * from './ExpoBrownfieldModule';\nexport * from './ExpoBrownfieldStateModule';\n"]} \ No newline at end of file diff --git a/packages/expo-brownfield/build/types.d.ts.map b/packages/expo-brownfield/build/types.d.ts.map deleted file mode 100644 index da0b63d9d39a85..00000000000000 --- a/packages/expo-brownfield/build/types.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE/C,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;AAE7C,MAAM,MAAM,0BAA0B,GAAG;IACvC,SAAS,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC1C,CAAC;AAEF,MAAM,CAAC,OAAO,OAAO,wBAAyB,SAAQ,YAAY,CAAC,0BAA0B,CAAC;IAC5F,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI;IACpC,oBAAoB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAC5C,WAAW,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;CAChD"} \ No newline at end of file diff --git a/packages/expo-brownfield/build/types.js b/packages/expo-brownfield/build/types.js deleted file mode 100644 index 718fd38ae40c67..00000000000000 --- a/packages/expo-brownfield/build/types.js +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/packages/expo-brownfield/build/types.js.map b/packages/expo-brownfield/build/types.js.map deleted file mode 100644 index 8a222fa592913a..00000000000000 --- a/packages/expo-brownfield/build/types.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { NativeModule } from 'expo';\n\nexport type MessageEvent = Record;\n\nexport type Listener = (event: E) => void;\n\nexport type ExpoBrownfieldModuleEvents = {\n onMessage: (event: MessageEvent) => void;\n};\n\nexport declare class ExpoBrownfieldModuleSpec extends NativeModule {\n popToNative(animated: boolean): void;\n setNativeBackEnabled(enabled: boolean): void;\n sendMessage(message: Record): void;\n}\n"]} \ No newline at end of file diff --git a/packages/expo-brownfield/expo-module.config.json b/packages/expo-brownfield/expo-module.config.json index efb65fa1a02d48..1ffe01a7cb748c 100644 --- a/packages/expo-brownfield/expo-module.config.json +++ b/packages/expo-brownfield/expo-module.config.json @@ -1,9 +1,15 @@ { "platforms": ["apple", "android", "web"], "apple": { - "modules": ["ExpoBrownfieldModule"] + "modules": [ + "ExpoBrownfieldModule", + "ExpoBrownfieldStateModule" + ] }, "android": { - "modules": ["expo.modules.brownfield.ExpoBrownfieldModule"] + "modules": [ + "expo.modules.brownfield.ExpoBrownfieldModule", + "expo.modules.brownfield.ExpoBrownfieldStateModule" + ] } } diff --git a/packages/expo-brownfield/ios/ExpoBrownfieldStateModule.swift b/packages/expo-brownfield/ios/ExpoBrownfieldStateModule.swift new file mode 100644 index 00000000000000..b62bb3f458c7bb --- /dev/null +++ b/packages/expo-brownfield/ios/ExpoBrownfieldStateModule.swift @@ -0,0 +1,19 @@ +import ExpoModulesCore + +// MARK: - ExpoBrownfieldStateModule + +public class ExpoBrownfieldStateModule: Module { + private var stores: [String: Any] = [:] + + public func definition() -> ModuleDefinition { + Name("ExpoBrownfieldStateModule") + + Function("getSharedState") { (key: String) -> Any? in + return nil + } + + Function("deleteSharedState") { (key: String) -> Void in + self.stores.removeValue(forKey: key) + } + } +} diff --git a/packages/expo-brownfield/package.json b/packages/expo-brownfield/package.json index 8c8b10bd712404..c80098db2baafc 100644 --- a/packages/expo-brownfield/package.json +++ b/packages/expo-brownfield/package.json @@ -64,6 +64,7 @@ "expo-module-scripts": "^55.0.2" }, "peerDependencies": { - "expo": "*" + "expo": "*", + "react": "*" } } diff --git a/packages/expo-brownfield/src/ExpoBrownfieldModule.ts b/packages/expo-brownfield/src/ExpoBrownfieldModule.ts index 1a3762e1449e42..66c5b8321cf7be 100644 --- a/packages/expo-brownfield/src/ExpoBrownfieldModule.ts +++ b/packages/expo-brownfield/src/ExpoBrownfieldModule.ts @@ -1,5 +1,97 @@ import { requireNativeModule } from 'expo'; +import type { EventSubscription } from 'expo-modules-core'; -import type { ExpoBrownfieldModuleSpec } from './types'; +import type { + ExpoBrownfieldModuleSpec, + Listener, + MessageEvent, +} from './ExpoBrownfieldModule.types'; -export default requireNativeModule('ExpoBrownfieldModule'); +const ExpoBrownfieldModule = requireNativeModule('ExpoBrownfieldModule'); + +export { EventSubscription }; +export type { MessageEvent }; + +// SECTION: Navigation API + +/** + * Navigates back to the native part of the app, dismissing the React Native view. + * + * @param animated Whether to animate the transition (iOS only). Defaults to `false`. + * @default false + */ +export function popToNative(animated: boolean = false): void { + ExpoBrownfieldModule.popToNative(animated); +} + +/** + * Enables or disables the native back button behavior. When enabled, pressing the + * back button will navigate back to the native part of the app instead of + * performing the default React Navigation back action. + * + * @param enabled Whether to enable native back button handling. + */ +export function setNativeBackEnabled(enabled: boolean): void { + ExpoBrownfieldModule.setNativeBackEnabled(enabled); +} + +// END SECTION: Navigation API + +// SECTION: Messaging API + +/** + * Adds a listener for messages sent from the native side of the app. + * + * @param listener A callback function that receives message events from native. + * @returns A subscription object that can be used to remove the listener. + * + * @example + * ```ts + * const subscription = addMessageListener((event) => { + * console.log('Received message from native:', event); + * }); + * + * // Later, to remove the listener: + * subscription.remove(); + * ``` + */ +export function addMessageListener(listener: Listener): EventSubscription { + return ExpoBrownfieldModule.addListener('onMessage', listener); +} + +/** + * Sends a message to the native side of the app. The message can be received by + * setting up a listener in the native code. + * + * @param message A dictionary containing the message payload to send to native. + */ +export function sendMessage(message: Record): void { + ExpoBrownfieldModule.sendMessage(message); +} + +/** + * Removes a specific message listener. + * + * @param listener The listener function to remove. + */ +export function removeMessageListener(listener: Listener): void { + ExpoBrownfieldModule.removeListener('onMessage', listener); +} + +/** + * Removes all message listeners. + */ +export function removeAllMessageListeners(): void { + ExpoBrownfieldModule.removeAllListeners('onMessage'); +} + +/** + * Gets the number of registered message listeners. + * + * @returns The number of active message listeners. + */ +export function getMessageListenerCount(): number { + return ExpoBrownfieldModule.listenerCount('onMessage'); +} + +// END SECTION: Messaging API diff --git a/packages/expo-brownfield/src/types.ts b/packages/expo-brownfield/src/ExpoBrownfieldModule.types.ts similarity index 83% rename from packages/expo-brownfield/src/types.ts rename to packages/expo-brownfield/src/ExpoBrownfieldModule.types.ts index 0e10134dd858cf..da0985b876686d 100644 --- a/packages/expo-brownfield/src/types.ts +++ b/packages/expo-brownfield/src/ExpoBrownfieldModule.types.ts @@ -4,11 +4,11 @@ export type MessageEvent = Record; export type Listener = (event: E) => void; -export type ExpoBrownfieldModuleEvents = { +export type Events = { onMessage: (event: MessageEvent) => void; }; -export declare class ExpoBrownfieldModuleSpec extends NativeModule { +export declare class ExpoBrownfieldModuleSpec extends NativeModule { popToNative(animated: boolean): void; setNativeBackEnabled(enabled: boolean): void; sendMessage(message: Record): void; diff --git a/packages/expo-brownfield/src/ExpoBrownfieldStateModule.ts b/packages/expo-brownfield/src/ExpoBrownfieldStateModule.ts new file mode 100644 index 00000000000000..8f1f4e4849fcf2 --- /dev/null +++ b/packages/expo-brownfield/src/ExpoBrownfieldStateModule.ts @@ -0,0 +1,124 @@ +import { requireNativeModule } from 'expo'; +import type { EventSubscription } from 'expo-modules-core'; +import { useEffect, useState } from 'react'; + +import type { ExpoBrownfieldStateModuleSpec } from './ExpoBrownfieldStateModule.types'; + +const ExpoBrownfieldStateModule = requireNativeModule( + 'ExpoBrownfieldStateModule' +); + +const sharedObjectCache = new Map(); + +// SECTION: Shared State API + +function getSharedObject(key: string): any { + if (!sharedObjectCache.has(key)) { + sharedObjectCache.set(key, ExpoBrownfieldStateModule.getSharedState(key)); + } + return sharedObjectCache.get(key); +} + +/** + * Gets the value of shared state for a given key. + * + * @param key The key to get the value for. + */ +export function getSharedStateValue(key: string): T | undefined { + const state = getSharedObject(key); + const value = state?.get(); + return value === null ? undefined : (value as T); +} + +/** + * Sets the value of shared state for a given key. + * + * @param key The key to set the value for. + * @param value The value to be set. + */ +export function setSharedStateValue(key: string, value: T): void { + const state = getSharedObject(key); + state.set(value); +} + +/** + * Deletes the shared state for a given key. + * + * @param key The key to delete the shared state for. + */ +export function deleteSharedState(key: string): void { + ExpoBrownfieldStateModule.deleteSharedState(key); + sharedObjectCache.delete(key); +} + +/** + * Adds a listener for changes to the shared state for a given key. + * + * @param key The key to add the listener for. + * @param callback The callback to be called when the shared state changes. + * @returns A subscription object that can be used to remove the listener. + */ +export function addSharedStateListener( + key: string, + callback: (value: T | undefined) => void +): EventSubscription { + const state = getSharedObject(key); + + const subscription = state.addListener('change', (event: T | undefined) => { + callback(event); + }); + + return { + remove: () => subscription.remove(), + }; +} + +/** + * Hook to observe and set the value of shared state for a given key. + * Provides a synchronous API similar to `useState`. + * + * @param key The key to get the value for. + * @param initialValue The initial value to be used if the shared state is not set. + * @returns A tuple containing the value and a function to set the value. + */ +export function useSharedState( + key: string, + initialValue?: T +): [T | undefined, (value: T | ((prev: T | undefined) => T)) => void] { + const state = getSharedObject(key); + + const [value, setValue] = useState(() => { + const currentValue = state.get(); + if (currentValue === null || currentValue === undefined) { + if (initialValue !== undefined) { + state.set(initialValue); + return initialValue; + } + + return undefined; + } + + return currentValue as T; + }); + + useEffect(() => { + const subscription = state.addListener( + 'change', + (event: Record | undefined) => { + setValue(event?.['value']); + } + ); + + return () => subscription.remove(); + }, [state]); + + const setSharedValue = (newValue: T | ((prev: T | undefined) => T)) => { + const valueToSet = + typeof newValue === 'function' ? (newValue as (prev: T | undefined) => T)(value) : newValue; + state.set(valueToSet); + }; + + return [value, setSharedValue]; +} + +// END SECTION: Shared State API diff --git a/packages/expo-brownfield/src/ExpoBrownfieldStateModule.types.ts b/packages/expo-brownfield/src/ExpoBrownfieldStateModule.types.ts new file mode 100644 index 00000000000000..198009eccb8603 --- /dev/null +++ b/packages/expo-brownfield/src/ExpoBrownfieldStateModule.types.ts @@ -0,0 +1,6 @@ +import type { NativeModule } from 'expo'; + +export declare class ExpoBrownfieldStateModuleSpec extends NativeModule { + getSharedState(key: string): any; + deleteSharedState(key: string): void; +} diff --git a/packages/expo-brownfield/src/index.ts b/packages/expo-brownfield/src/index.ts index 6640dbbc2b8060..6401d508c88b95 100644 --- a/packages/expo-brownfield/src/index.ts +++ b/packages/expo-brownfield/src/index.ts @@ -1,83 +1,2 @@ -import type { EventSubscription } from 'expo-modules-core'; - -import ExpoBrownfieldModule from './ExpoBrownfieldModule'; -import type { Listener, MessageEvent } from './types'; - -export { EventSubscription }; -export type { MessageEvent }; - -/** - * Navigates back to the native part of the app, dismissing the React Native view. - * - * @param animated Whether to animate the transition (iOS only). Defaults to `false`. - * @default false - */ -export function popToNative(animated: boolean = false): void { - ExpoBrownfieldModule.popToNative(animated); -} - -/** - * Enables or disables the native back button behavior. When enabled, pressing the - * back button will navigate back to the native part of the app instead of - * performing the default React Navigation back action. - * - * @param enabled Whether to enable native back button handling. - */ -export function setNativeBackEnabled(enabled: boolean): void { - ExpoBrownfieldModule.setNativeBackEnabled(enabled); -} - -/** - * Adds a listener for messages sent from the native side of the app. - * - * @param listener A callback function that receives message events from native. - * @returns A subscription object that can be used to remove the listener. - * - * @example - * ```ts - * const subscription = addMessageListener((event) => { - * console.log('Received message from native:', event); - * }); - * - * // Later, to remove the listener: - * subscription.remove(); - * ``` - */ -export function addMessageListener(listener: Listener): EventSubscription { - return ExpoBrownfieldModule.addListener('onMessage', listener); -} - -/** - * Sends a message to the native side of the app. The message can be received by - * setting up a listener in the native code. - * - * @param message A dictionary containing the message payload to send to native. - */ -export function sendMessage(message: Record): void { - ExpoBrownfieldModule.sendMessage(message); -} - -/** - * Removes a specific message listener. - * - * @param listener The listener function to remove. - */ -export function removeMessageListener(listener: Listener): void { - ExpoBrownfieldModule.removeListener('onMessage', listener); -} - -/** - * Removes all message listeners. - */ -export function removeAllMessageListeners(): void { - ExpoBrownfieldModule.removeAllListeners('onMessage'); -} - -/** - * Gets the number of registered message listeners. - * - * @returns The number of active message listeners. - */ -export function getMessageListenerCount(): number { - return ExpoBrownfieldModule.listenerCount('onMessage'); -} +export * from './ExpoBrownfieldModule'; +export * from './ExpoBrownfieldStateModule';