REF: migrate SegmentedControl to New Architecture

This commit is contained in:
Marcos Rodriguez 2026-04-27 21:20:35 -05:00
parent 038cabedaf
commit e098e89dc3
24 changed files with 345 additions and 274 deletions

View File

@ -102,6 +102,7 @@ android {
sourceSets {
main {
assets.srcDirs = ['src/main/assets', 'src/main/res/assets']
java.srcDirs = ['src/main/java', '../../blue_modules/Views/SegmentedControl/android']
}
}

View File

@ -15,7 +15,7 @@ import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.modules.i18nmanager.I18nUtil
import io.bluewallet.bluewallet.components.segmentedcontrol.CustomSegmentedControlPackage
import io.bluewallet.bluewallet.components.segmentedcontrol.SegmentedControlPackage
class MainApplication : Application(), ReactApplication {
@ -69,7 +69,7 @@ class MainApplication : Application(), ReactApplication {
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage())
add(CustomSegmentedControlPackage())
add(SegmentedControlPackage())
add(SettingsPackage())
}

View File

@ -1,83 +0,0 @@
package io.bluewallet.bluewallet.components.segmentedcontrol
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.viewmanagers.CustomSegmentedControlManagerInterface
@ReactModule(name = CustomSegmentedControlManager.REACT_CLASS)
class CustomSegmentedControlManager : SimpleViewManager<CustomSegmentedControl>(),
CustomSegmentedControlManagerInterface<CustomSegmentedControl> {
companion object {
const val REACT_CLASS = "CustomSegmentedControl"
private const val TOP_CHANGE = "topChange"
private const val REGISTRATION_NAME = "onChange"
}
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): CustomSegmentedControl {
return CustomSegmentedControl(reactContext)
}
@ReactProp(name = "values")
override fun setValues(view: CustomSegmentedControl, values: ReadableArray?) {
val valuesArray = values?.let { array ->
Array(array.size()) { index ->
array.getString(index) ?: ""
}
} ?: emptyArray()
view.values = valuesArray
}
@ReactProp(name = "selectedIndex", defaultInt = 0)
override fun setSelectedIndex(view: CustomSegmentedControl, selectedIndex: Int) {
view.selectedIndex = selectedIndex
}
@ReactProp(name = "backgroundColor")
override fun setBackgroundColor(view: CustomSegmentedControl, value: String?) {
view.setBackgroundColorProp(value)
}
@ReactProp(name = "tintColor")
override fun setTintColor(view: CustomSegmentedControl, value: String?) {
view.setTintColorProp(value)
}
@ReactProp(name = "textColor")
override fun setTextColor(view: CustomSegmentedControl, value: String?) {
view.setTextColorProp(value)
}
@ReactProp(name = "momentary", defaultBoolean = false)
override fun setMomentary(view: CustomSegmentedControl, value: Boolean) {
view.setMomentaryProp(value)
}
@ReactProp(name = "enabled", defaultBoolean = true)
override fun setEnabled(view: CustomSegmentedControl, value: Boolean) {
view.setEnabledProp(value)
}
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any>? {
return MapBuilder.builder<String, Any>()
.put(
TOP_CHANGE,
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", REGISTRATION_NAME, "captured", "${REGISTRATION_NAME}Capture")
)
)
.build()
}
override fun onAfterUpdateTransaction(view: CustomSegmentedControl) {
super.onAfterUpdateTransaction(view)
}
}

View File

@ -9,13 +9,13 @@ import androidx.core.content.ContextCompat
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReactContext
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.Event
import com.google.android.material.button.MaterialButton
import com.google.android.material.button.MaterialButtonToggleGroup
import io.bluewallet.bluewallet.R
class CustomSegmentedControl @JvmOverloads constructor(
class SegmentedControl @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
@ -48,10 +48,13 @@ class CustomSegmentedControl @JvmOverloads constructor(
isSingleSelection = true
isSelectionRequired = true
}
addView(toggleGroup, LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
))
addView(
toggleGroup,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT,
),
)
toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (isChecked) {
@ -69,29 +72,29 @@ class CustomSegmentedControl @JvmOverloads constructor(
private fun updateSegments() {
toggleGroup.removeAllViews()
values.forEachIndexed { index, title ->
val button = MaterialButton(
context,
null,
com.google.android.material.R.attr.materialButtonOutlinedStyle
com.google.android.material.R.attr.materialButtonOutlinedStyle,
).apply {
text = title
id = generateViewId()
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
1f,
)
isCheckable = true
strokeWidth = 2
applyEnabledState()
val cornerRadius = resources.getDimensionPixelSize(
com.google.android.material.R.dimen.mtrl_btn_corner_radius
com.google.android.material.R.dimen.mtrl_btn_corner_radius,
)
when {
values.size == 1 -> {
this.cornerRadius = cornerRadius
@ -107,10 +110,10 @@ class CustomSegmentedControl @JvmOverloads constructor(
}
}
}
toggleGroup.addView(button)
}
updateButtonColors()
updateSelectedSegment()
}
@ -118,7 +121,7 @@ class CustomSegmentedControl @JvmOverloads constructor(
private fun updateButtonColors() {
for (i in 0 until toggleGroup.childCount) {
val button = toggleGroup.getChildAt(i) as? MaterialButton ?: continue
val selectedBgColor = tintColorProp ?: ContextCompat.getColor(context, R.color.button_background_color)
val unselectedBgColor = backgroundColorProp ?: ContextCompat.getColor(context, R.color.button_disabled_background_color)
val resolvedTextColor = textColorProp ?: ContextCompat.getColor(context, R.color.button_text_color)
@ -127,39 +130,39 @@ class CustomSegmentedControl @JvmOverloads constructor(
val borderColor = ContextCompat.getColor(context, R.color.form_border_color)
val rippleColor = ContextCompat.getColor(context, R.color.ripple_color)
val rippleColorSelected = ContextCompat.getColor(context, R.color.ripple_color_selected)
val bgColorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
intArrayOf(-android.R.attr.state_checked),
),
intArrayOf(selectedBgColor, unselectedBgColor)
intArrayOf(selectedBgColor, unselectedBgColor),
)
val textColorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked),
),
intArrayOf(selectedTextColor, unselectedTextColor)
intArrayOf(selectedTextColor, unselectedTextColor),
)
val strokeColorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked),
),
intArrayOf(borderColor, borderColor)
intArrayOf(borderColor, borderColor),
)
val rippleColorStateList = ColorStateList(
arrayOf(
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked)
intArrayOf(android.R.attr.state_checked),
intArrayOf(-android.R.attr.state_checked),
),
intArrayOf(rippleColorSelected, rippleColor)
intArrayOf(rippleColorSelected, rippleColor),
)
button.backgroundTintList = bgColorStateList
button.setTextColor(textColorStateList)
button.strokeColor = strokeColorStateList
@ -224,11 +227,11 @@ class CustomSegmentedControl @JvmOverloads constructor(
val reactContext = context as? ReactContext ?: return
val surfaceId = UIManagerHelper.getSurfaceId(reactContext)
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, id)
val event = Arguments.createMap().apply {
putInt("selectedIndex", selectedIndex)
}
eventDispatcher?.dispatchEvent(ChangeEvent(surfaceId, id, event))
}
@ -250,11 +253,11 @@ class CustomSegmentedControl @JvmOverloads constructor(
private inner class ChangeEvent(
surfaceId: Int,
viewId: Int,
private val eventData: WritableMap
private val eventData: WritableMap,
) : Event<ChangeEvent>(surfaceId, viewId) {
override fun getEventName(): String = "topChange"
override fun getEventData(): WritableMap = eventData
}
}
}

View File

@ -0,0 +1,67 @@
package io.bluewallet.bluewallet.components.segmentedcontrol
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.common.MapBuilder
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.annotations.ReactProp
@ReactModule(name = SegmentedControlManager.REACT_CLASS)
class SegmentedControlManager : SimpleViewManager<SegmentedControl>() {
companion object {
const val REACT_CLASS = "SegmentedControl"
}
override fun getName(): String = REACT_CLASS
override fun createViewInstance(reactContext: ThemedReactContext): SegmentedControl =
SegmentedControl(reactContext)
@ReactProp(name = "values")
fun setValues(view: SegmentedControl, values: ReadableArray?) {
view.values = values?.let { arr -> Array(arr.size()) { arr.getString(it) ?: "" } } ?: emptyArray()
}
@ReactProp(name = "selectedIndex", defaultInt = 0)
fun setSelectedIndex(view: SegmentedControl, selectedIndex: Int) {
view.selectedIndex = selectedIndex
}
@ReactProp(name = "enabled", defaultBoolean = true)
fun setEnabled(view: SegmentedControl, enabled: Boolean) {
view.setEnabledProp(enabled)
}
@ReactProp(name = "momentary", defaultBoolean = false)
fun setMomentary(view: SegmentedControl, momentary: Boolean) {
view.setMomentaryProp(momentary)
}
@ReactProp(name = "backgroundColor")
fun setBackgroundColor(view: SegmentedControl, backgroundColor: String?) {
view.setBackgroundColorProp(backgroundColor)
}
@ReactProp(name = "tintColor")
fun setTintColor(view: SegmentedControl, tintColor: String?) {
view.setTintColorProp(tintColor)
}
@ReactProp(name = "textColor")
fun setTextColor(view: SegmentedControl, textColor: String?) {
view.setTextColorProp(textColor)
}
override fun getExportedCustomBubblingEventTypeConstants(): Map<String, Any>? =
MapBuilder.builder<String, Any>()
.put(
"topChange",
MapBuilder.of(
"phasedRegistrationNames",
MapBuilder.of("bubbled", "onChange", "captured", "onChangeCapture"),
),
)
.build()
}

View File

@ -5,13 +5,13 @@ import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
class CustomSegmentedControlPackage : ReactPackage {
class SegmentedControlPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return emptyList()
}
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
return listOf(CustomSegmentedControlManager())
return listOf(SegmentedControlManager())
}
}
}

View File

@ -0,0 +1,6 @@
#import <React/RCTViewManager.h>
@interface RCT_EXTERN_MODULE(SegmentedControlManager, RCTViewManager)
@end

View File

@ -0,0 +1,23 @@
import Foundation
import UIKit
import React
@objc(SegmentedControlManager)
final class SegmentedControlManager: RCTViewManager {
override class func requiresMainQueueSetup() -> Bool { true }
override func view() -> UIView! {
return SegmentedControlView()
}
@objc class func propConfig_values() -> [String]! { ["NSArray"] }
@objc class func propConfig_selectedIndex() -> [String]! { ["NSInteger"] }
@objc class func propConfig_enabled() -> [String]! { ["BOOL"] }
@objc class func propConfig_momentary() -> [String]! { ["BOOL"] }
@objc class func propConfig_tintColor() -> [String]! { ["UIColor"] }
@objc class func propConfig_backgroundColor() -> [String]! { ["UIColor"] }
@objc class func propConfig_textColor() -> [String]! { ["UIColor"] }
@objc class func propConfig_onChange() -> [String]! { ["RCTBubblingEventBlock"] }
}

View File

@ -0,0 +1,98 @@
import UIKit
import React
@objc(SegmentedControlView)
final class SegmentedControlView: UIView {
private let segmentedControl = UISegmentedControl()
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
setup()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
private func setup() {
segmentedControl.addTarget(self, action: #selector(handleValueChanged(_:)), for: .valueChanged)
addSubview(segmentedControl)
segmentedControl.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
segmentedControl.leadingAnchor.constraint(equalTo: leadingAnchor),
segmentedControl.trailingAnchor.constraint(equalTo: trailingAnchor),
segmentedControl.topAnchor.constraint(equalTo: topAnchor),
segmentedControl.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
// MARK: - Prop setters
@objc var values: NSArray = [] {
didSet { rebuildSegments() }
}
@objc var selectedIndex: Int = 0 {
didSet {
guard segmentedControl.numberOfSegments > 0 else { return }
let clamped = min(max(selectedIndex, 0), segmentedControl.numberOfSegments - 1)
if segmentedControl.selectedSegmentIndex != clamped {
segmentedControl.selectedSegmentIndex = clamped
}
}
}
@objc var enabled: Bool = true {
didSet { segmentedControl.isEnabled = enabled }
}
@objc var momentary: Bool = false {
didSet { segmentedControl.isMomentary = momentary }
}
@objc var textColor: UIColor? {
didSet { applyTextAttributes() }
}
@objc var onChange: RCTBubblingEventBlock?
override var tintColor: UIColor! {
didSet { segmentedControl.selectedSegmentTintColor = tintColor }
}
override var backgroundColor: UIColor? {
didSet { segmentedControl.backgroundColor = backgroundColor }
}
// MARK: - Private helpers
private func rebuildSegments() {
let titles = values as? [String] ?? []
segmentedControl.removeAllSegments()
for (i, title) in titles.enumerated() {
segmentedControl.insertSegment(withTitle: title, at: i, animated: false)
}
guard !titles.isEmpty else { return }
let clamped = min(max(selectedIndex, 0), titles.count - 1)
segmentedControl.selectedSegmentIndex = clamped
}
private func applyTextAttributes() {
if let color = textColor {
segmentedControl.setTitleTextAttributes([.foregroundColor: color], for: .normal)
segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
} else {
segmentedControl.setTitleTextAttributes(nil, for: .normal)
segmentedControl.setTitleTextAttributes(nil, for: .selected)
}
}
@objc private func handleValueChanged(_ sender: UISegmentedControl) {
onChange?(["selectedIndex": sender.selectedSegmentIndex])
}
}

View File

@ -1,16 +0,0 @@
import type { ViewProps } from 'react-native';
import type { BubblingEventHandler, Int32 } from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
export interface NativeProps extends ViewProps {
values: string[];
selectedIndex: Int32;
enabled: boolean;
backgroundColor: string;
tintColor: string;
textColor: string;
momentary: boolean;
onChange?: BubblingEventHandler<Readonly<{ selectedIndex: Int32 }>>;
}
export default codegenNativeComponent<NativeProps>('CustomSegmentedControl');

View File

@ -0,0 +1,22 @@
import type { HostComponent } from 'react-native';
import type { ViewProps } from 'react-native';
import type { BubblingEventHandler, Int32, WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
type SegmentedControlChangeEvent = Readonly<{
selectedIndex: Int32;
target: Int32;
}>;
export interface NativeProps extends ViewProps {
values?: ReadonlyArray<string>;
selectedIndex?: WithDefault<Int32, 0>;
enabled?: WithDefault<boolean, true>;
backgroundColor?: string | null;
tintColor?: string | null;
textColor?: string | null;
momentary?: WithDefault<boolean, false>;
onChange?: BubblingEventHandler<SegmentedControlChangeEvent> | null;
}
export default codegenNativeComponent<NativeProps>('SegmentedControl') as HostComponent<NativeProps>;

View File

@ -2,12 +2,10 @@ import React, { forwardRef, ReactNode, useEffect, useRef, useState, useCallback,
import {
Animated,
LayoutAnimation,
Platform,
PixelRatio,
StyleSheet,
Text,
TouchableOpacity,
UIManager,
useWindowDimensions,
View,
StyleProp,
@ -19,10 +17,6 @@ import { useSizeClass, SizeClass } from '../blue_modules/sizeClass';
import { isDesktop } from '../blue_modules/environment';
import debounce from '../blue_modules/debounce';
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const scheduleInNextFrame = (callback: () => void): number => {
return requestAnimationFrame(() => {
// Use a second requestAnimationFrame to ensure we're not in the same frame

View File

@ -1,20 +1,21 @@
import React, { useMemo } from 'react';
import React, { useCallback } from 'react';
import { View, StyleSheet, NativeSyntheticEvent } from 'react-native';
import NativeSegmentedControl from '../codegen/SegmentControlNativeComponent';
import NativeSegmentedControl from '../codegen/SegmentedControlNativeComponent';
interface SegmentedControlProps {
values: string[];
selectedIndex: number;
onChange: (index: number) => void;
testID?: string;
}
interface SegmentedControlEvent {
selectedIndex: number;
}
const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedIndex, onChange }) => {
const handleChange = useMemo(
() => (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedIndex, onChange, testID }) => {
const handleChange = useCallback(
(event: NativeSyntheticEvent<SegmentedControlEvent>) => {
if (event?.nativeEvent?.selectedIndex !== undefined) {
onChange(event.nativeEvent.selectedIndex);
}
@ -38,6 +39,7 @@ const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedInd
momentary={false}
style={styles.segmentedControl}
onChange={handleChange}
testID={testID}
/>
</View>
);

View File

@ -4,7 +4,7 @@ import 'react-native-get-random-values';
import './shim.js';
import React, { useEffect } from 'react';
import { AppRegistry, LogBox } from 'react-native';
import { AppRegistry, LogBox, Platform, UIManager } from 'react-native';
import App from './App';
import { restoreSavedPreferredFiatCurrencyAndExchangeFromStorage } from './blue_modules/currency';
@ -14,6 +14,10 @@ if (!Error.captureStackTrace) {
Error.captureStackTrace = () => {};
}
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
LogBox.ignoreLogs([
'Require cycle:',
'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.',

View File

@ -148,8 +148,10 @@
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
B4B1A4642BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */; };
B4B3EC222D69FF6C00327F3D /* SegmentedControlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC202D69FF6C00327F3D /* SegmentedControlView.swift */; };
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC232D69FF8700327F3D /* EventEmitter.swift */; };
B4B3EF102D6AFF6C003270A0 /* SegmentedControlManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EF002D6AFF6C003270A0 /* SegmentedControlManager.swift */; };
B4B3EF202D6CFF6C003270A0 /* SegmentedControlBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EF1F2D6CFF6C003270A0 /* SegmentedControlBridge.m */; };
B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */; };
B4D0B2642C1DEA99006B6B1B /* ReceiveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */; };
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */; };
@ -159,7 +161,6 @@
B4D59C1E2D8BAFE300B7025B /* BugsnagNetworkRequestPlugin in Frameworks */ = {isa = PBXBuildFile; productRef = B4D59C1D2D8BAFE300B7025B /* BugsnagNetworkRequestPlugin */; };
B4D59C212D8BB42100B7025B /* File.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D59C202D8BB41F00B7025B /* File.swift */; };
B4D59C272D8C5D6F00B7025B /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D59C262D8C5D6E00B7025B /* main.swift */; };
B4D899942DCAE67700B959AA /* CustomSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = B4D899932DCAE67700B959AA /* CustomSegmentedControl.m */; };
B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
B4F0A4A22FA1BC0000AAAA01 /* WidgetHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = B4F0A4A12FA1BC0000AAAA00 /* WidgetHelper.mm */; };
@ -366,8 +367,10 @@
B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = "<group>"; };
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = "<group>"; };
B4B31A352C77BBA000663334 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Interface.strings; sourceTree = "<group>"; };
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = CustomSegmentedControl.swift; path = SegmentedControl/CustomSegmentedControl.swift; sourceTree = "<group>"; };
B4B3EC202D69FF6C00327F3D /* SegmentedControlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SegmentedControlView.swift; path = ../blue_modules/Views/SegmentedControl/ios/SegmentedControlView.swift; sourceTree = SOURCE_ROOT; };
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventEmitter.swift; sourceTree = "<group>"; };
B4B3EF002D6AFF6C003270A0 /* SegmentedControlManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SegmentedControlManager.swift; path = ../blue_modules/Views/SegmentedControl/ios/SegmentedControlManager.swift; sourceTree = SOURCE_ROOT; };
B4B3EF1F2D6CFF6C003270A0 /* SegmentedControlBridge.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SegmentedControlBridge.m; path = ../blue_modules/Views/SegmentedControl/ios/SegmentedControlBridge.m; sourceTree = SOURCE_ROOT; };
B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceivePageInterfaceController.swift; sourceTree = "<group>"; };
B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveType.swift; sourceTree = "<group>"; };
B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInterfaceMode.swift; sourceTree = "<group>"; };
@ -375,7 +378,6 @@
B4D3235A177F4580BA52F2F9 /* libRNCSlider.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCSlider.a; sourceTree = "<group>"; };
B4D59C202D8BB41F00B7025B /* File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = File.swift; sourceTree = "<group>"; };
B4D59C262D8C5D6E00B7025B /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
B4D899932DCAE67700B959AA /* CustomSegmentedControl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControl.m; sourceTree = "<group>"; };
B4EFF73A2C3F6C5E0095D655 /* MockData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockData.swift; sourceTree = "<group>"; };
B4F0A4A12FA1BC0000AAAA00 /* WidgetHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = WidgetHelper.mm; sourceTree = "<group>"; };
B4F0A4A32FA1BC0000AAAA02 /* EventEmitter.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = EventEmitter.mm; sourceTree = "<group>"; };
@ -746,12 +748,13 @@
B45010A12C1504E900619044 /* Components */ = {
isa = PBXGroup;
children = (
B4D899932DCAE67700B959AA /* CustomSegmentedControl.m */,
B4AA75232DAA339E00CF5CBE /* MenuElementsEmitter.m */,
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */,
B4F0A4A32FA1BC0000AAAA02 /* EventEmitter.mm */,
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */,
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */,
B4B3EC202D69FF6C00327F3D /* SegmentedControlView.swift */,
B4B3EF002D6AFF6C003270A0 /* SegmentedControlManager.swift */,
B4B3EF1F2D6CFF6C003270A0 /* SegmentedControlBridge.m */,
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
B4F0A4A12FA1BC0000AAAA00 /* WidgetHelper.mm */,
);
@ -1162,10 +1165,11 @@
B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */,
B44034072BCC38A000162242 /* FiatUnit.swift in Sources */,
B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */,
B4D899942DCAE67700B959AA /* CustomSegmentedControl.m in Sources */,
B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */,
B4793DBB2CEDACBD00C92C2E /* Chain.swift in Sources */,
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */,
B4B3EC222D69FF6C00327F3D /* SegmentedControlView.swift in Sources */,
B4B3EF102D6AFF6C003270A0 /* SegmentedControlManager.swift in Sources */,
B4B3EF202D6CFF6C003270A0 /* SegmentedControlBridge.m in Sources */,
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */,
B4F0A4A22FA1BC0000AAAA01 /* WidgetHelper.mm in Sources */,
B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,

View File

@ -1,21 +0,0 @@
//
// RCT.h
// BlueWallet
//
// Created by Marcos Rodriguez on 4/22/25.
// Copyright © 2025 BlueWallet. All rights reserved.
//
#import <React/RCTViewManager.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTUIManager.h>
#import <React/RCTEventDispatcher.h>
@interface RCT_EXTERN_MODULE(CustomSegmentedControlManager, RCTViewManager)
RCT_EXPORT_VIEW_PROPERTY(values, NSArray)
RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
@end

View File

@ -1,47 +0,0 @@
import UIKit
import React
@objc(CustomSegmentedControl)
class CustomSegmentedControl: UISegmentedControl {
@objc var onChange: RCTBubblingEventBlock?
@objc var values: [String] = [] {
didSet {
removeAllSegments()
for (index, title) in values.enumerated() {
insertSegment(withTitle: title, at: index, animated: false)
}
}
}
@objc var selectedIndex: NSNumber = 0 {
didSet {
self.selectedSegmentIndex = selectedIndex.intValue
}
}
override init(frame: CGRect) {
super.init(frame: frame)
addTarget(self, action: #selector(onChange(_:)), for: .valueChanged)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
addTarget(self, action: #selector(onChange(_:)), for: .valueChanged)
}
@objc func onChange(_ sender: UISegmentedControl) {
onChange?(["selectedIndex": sender.selectedSegmentIndex])
}
}
@objc(CustomSegmentedControlManager)
class CustomSegmentedControlManager: RCTViewManager {
override func view() -> UIView! {
return CustomSegmentedControl(frame: .zero)
}
override class func requiresMainQueueSetup() -> Bool {
return true
}
}

View File

@ -1991,7 +1991,7 @@ PODS:
- React-perflogger (= 0.84.1)
- React-utils (= 0.84.1)
- ReactNativeDependencies
- ReactNativeCameraKit (17.0.3):
- ReactNativeCameraKit (17.0.4):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@ -2323,7 +2323,7 @@ PODS:
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- Yoga
- RNSVG (15.15.3):
- RNSVG (15.15.4):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@ -2344,9 +2344,9 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- ReactNativeDependencies
- RNSVG/common (= 15.15.3)
- RNSVG/common (= 15.15.4)
- Yoga
- RNSVG/common (15.15.3):
- RNSVG/common (15.15.4):
- hermes-engine
- RCTRequired
- RCTTypeSafety
@ -2801,7 +2801,7 @@ SPEC CHECKSUMS:
BVLinearGradient: cb006ba232a1f3e4f341bb62c42d1098c284da70
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
FBLazyVector: e97c19a5a442429d1988f182a1940fb08df514da
hermes-engine: 5b6b255f9e4ba0f6f699433393c9ca57c402fc96
hermes-engine: 26a52ac276936854ac2797880e2c181aa1fb0025
lottie-ios: 8f959969761e9c45d70353667d00af0e5b9cadb3
lottie-react-native: 6a080b2f109ef611c75c503a33ebb8ea75db0c91
RCTDeprecation: af44b104091a34482596cd9bd7e8d90c4e9b4bd7
@ -2812,7 +2812,7 @@ SPEC CHECKSUMS:
React: 1ba7d364ade7d883a1ec055bfc3606f35fdee17b
React-callinvoker: bc2a26f8d84fb01f003fc6de6c9337b64715f95b
React-Core: 7840d3a80b43a95c5e80ef75146bd70925ebab0f
React-Core-prebuilt: fe445abdc8b577160a3ecf632edd6545f00f10cf
React-Core-prebuilt: 96b4b7f11b336f3b9d498eca3460c82ee260b31e
React-CoreModules: 2eb010400b63b89e53a324ffb3c112e4c7c3ce42
React-cxxreact: a558e92199d26f145afa9e62c4233cf8e7950efe
React-debug: 755200a6e7f5e6e0a40ff8d215493d43cce285fc
@ -2889,10 +2889,10 @@ SPEC CHECKSUMS:
React-utils: 8d888b379f0808bfabaea03d85f9e8dd9b8548da
React-webperformancenativemodule: c10016db7f1bb1153060d4aa9f7dbde2c88c845d
ReactAppDependencyProvider: e96e93b493d8d86eeaee3e590ba0be53f6abe46f
ReactCodegen: c8cd59a80d11b8c2f8e70fab3cdc62673ae56f1b
ReactCodegen: 333ec6399ca7299a87623ad07db17874502f9d64
ReactCommon: 07572bf9e687c8a52fbe4a3641e9e3a1a477c78e
ReactNativeCameraKit: 2f24ad111e0f525a6529dd0eff5bf5d4454a24ba
ReactNativeDependencies: 9f044a84d7bddd9822da88447b4a37e7cc085862
ReactNativeCameraKit: f4911c327342c1f34aae1346f9d5f5ae3030b0f6
ReactNativeDependencies: d91ea5381a1df88b4bc29bfb8a525ececa743c3d
RealmJS: 1c37c6bdfe060f4caa0f9175aa0eedb962622ee1
RNCAsyncStorage: 3a4f5e2777dae1688b781a487923a08569e27fe4
RNCClipboard: 88d7eeb555d1183915f0885bdbc5c97eb6f7f3ba
@ -2909,7 +2909,7 @@ SPEC CHECKSUMS:
RNReanimated: 66fa99647173f254f731e6aa59a0a2cc8c838ac1
RNScreens: 6cb648bdad8fe9bee9259fe144df95b6d1d5b707
RNShare: aad7f2a80ae80be5e2bf57583be28b1a18f95de1
RNSVG: 507bf2685de6b3d49449efd4aae7e7471bb9c433
RNSVG: c69f7709226108f5eb89b5aa8833c17a36345468
RNWatch: 28fe1f5e0c6410d45fd20925f4796fce05522e3f
RNWorklets: 30f9a363d681c776a5f9d74f6d21a6d1bb7bfe80
Yoga: c0b3f2c7e8d3e327e450223a2414ca3fa296b9a2

View File

@ -16,7 +16,7 @@ import CopyTextToClipboard from '../../components/CopyTextToClipboard';
import HandOffComponent from '../../components/HandOffComponent';
import HeaderMenuButton from '../../components/HeaderMenuButton';
import QRCodeComponent from '../../components/QRCodeComponent';
import SegmentedControl from '../../components/SegmentControl';
import SegmentedControl from '../../components/SegmentedControl';
import { useTheme } from '../../components/themes';
import TipBox from '../../components/TipBox';
import { TransactionPendingIconBig } from '../../components/TransactionPendingIconBig';

View File

@ -1,7 +1,7 @@
import dayjs from 'dayjs';
import calendar from 'dayjs/plugin/calendar';
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { Keyboard, LayoutAnimation, NativeSyntheticEvent, Platform, StyleSheet, UIManager, View } from 'react-native';
import { Keyboard, LayoutAnimation, NativeSyntheticEvent, StyleSheet, View } from 'react-native';
import {
CurrencyRate,
@ -26,10 +26,6 @@ import { FiatUnit, FiatUnitSource, FiatUnitType, getFiatRate } from '../../model
dayjs.extend(calendar);
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const MAX_DISPLAY_ITEMS = 50;
const Currency: React.FC = () => {

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { RouteProp, useRoute } from '@react-navigation/native';
import { ActivityIndicator, FlatList, LayoutAnimation, Platform, StyleSheet, UIManager, View } from 'react-native';
import { ActivityIndicator, FlatList, LayoutAnimation, Platform, StyleSheet, View } from 'react-native';
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
import { BlueButtonLink, BlueFormLabel, BlueText } from '../../BlueComponents';
import { HDSegwitBech32Wallet } from '../../class/wallets/hd-segwit-bech32-wallet';
@ -31,10 +31,6 @@ type WalletEntry = {
id: string;
};
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const ImportWalletDiscovery: React.FC = () => {
const navigation = useExtendedNavigation<NavigationProp>();
const { colors } = useTheme();

View File

@ -1,17 +1,5 @@
import React, { useEffect, useLayoutEffect, useReducer, useCallback, useMemo, useRef, useState, lazy, Suspense } from 'react';
import {
StyleSheet,
TouchableOpacity,
Image,
Alert,
Animated,
ActivityIndicator,
UIManager,
Platform,
Keyboard,
Text,
Pressable,
} from 'react-native';
import { StyleSheet, TouchableOpacity, Image, Alert, Animated, ActivityIndicator, Keyboard, Text, Pressable } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useLocale, usePreventRemove } from '@react-navigation/native';
@ -154,10 +142,6 @@ const reducer = (state: State, action: Action): State => {
}
};
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const ManageWallets: React.FC = () => {
const { colors, closeImage } = useTheme();
const { wallets: persistedWallets, setWalletsWithNewOrder, txMetadata } = useStorage();

View File

@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useLayoutEffect, useRef, useReducer, useMemo } from 'react';
import { useRoute, RouteProp, useFocusEffect } from '@react-navigation/native';
import { ActivityIndicator, FlatList, StyleSheet, View, Platform, UIManager } from 'react-native';
import { ActivityIndicator, FlatList, StyleSheet, View } from 'react-native';
import { WatchOnlyWallet } from '../../class/wallets/watch-only-wallet';
import { AddressItem } from '../../components/addresses/AddressItem';
import { useTheme } from '../../components/themes';
@ -8,7 +8,7 @@ import { useStorage } from '../../hooks/context/useStorage';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import SegmentedControl from '../../components/SegmentControl';
import SegmentedControl from '../../components/SegmentedControl';
import loc from '../../loc';
import { BitcoinUnit } from '../../models/bitcoinUnits';
import { useSettings } from '../../hooks/context/useSettings';
@ -21,10 +21,6 @@ export const TABS = {
type TabKey = keyof typeof TABS;
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
interface Address {
key: string;
index: number;

View File

@ -0,0 +1,42 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import SegmentedControl from '../../components/SegmentedControl';
jest.mock('../../codegen/SegmentedControlNativeComponent', () => {
const { View } = require('react-native');
const MockSegmentedControl = ({ values, selectedIndex, onChange, testID }: any) =>
React.createElement(View, { testID: testID ?? 'segmented-control' });
return {
__esModule: true,
default: MockSegmentedControl,
};
});
describe('SegmentedControl', () => {
const VALUES = ['One', 'Two', 'Three'];
it('renders without crashing', () => {
const { getByTestId } = render(<SegmentedControl values={VALUES} selectedIndex={0} onChange={jest.fn()} testID="sc" />);
expect(getByTestId('sc')).toBeTruthy();
});
it('returns null when values array is empty', () => {
const { toJSON } = render(<SegmentedControl values={[]} selectedIndex={0} onChange={jest.fn()} />);
expect(toJSON()).toBeNull();
});
it('passes values and selectedIndex to native component', () => {
const { getByTestId } = render(<SegmentedControl values={VALUES} selectedIndex={1} onChange={jest.fn()} testID="sc2" />);
expect(getByTestId('sc2')).toBeTruthy();
});
it('calls onChange with the correct index when selection changes', () => {
const onChangeMock = jest.fn();
const { UNSAFE_getAllByType } = render(<SegmentedControl values={VALUES} selectedIndex={0} onChange={onChangeMock} />);
const MockSegmentedControl = require('../../codegen/SegmentedControlNativeComponent').default;
const [instance] = UNSAFE_getAllByType(MockSegmentedControl);
// Simulate native onChange event
instance.props.onChange?.({ nativeEvent: { selectedIndex: 2 } });
expect(onChangeMock).toHaveBeenCalledWith(2);
});
});