diff --git a/android/app/build.gradle b/android/app/build.gradle index 77911e71c..548312c8d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -6,8 +6,8 @@ apply plugin: "com.google.firebase.crashlytics" import com.android.build.OutputFile -def canonicalVersionName = "3.0.1" -def canonicalVersionCode = 19 +def canonicalVersionName = "3.1.0" +def canonicalVersionCode = 29 // NOTE: DO NOT change postFixSize value, this is for handling legacy method for handling the versioning in android def postFixSize = 30_000 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1a7857974..e0159e81d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -61,11 +61,15 @@ + android:theme="@style/AppTheme" + + tools:targetApi="34"> - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/libs/common/InAppPurchaseModule.java b/android/app/src/main/java/libs/common/InAppPurchaseModule.java index 0daa38e62..7d9f657ad 100644 --- a/android/app/src/main/java/libs/common/InAppPurchaseModule.java +++ b/android/app/src/main/java/libs/common/InAppPurchaseModule.java @@ -6,8 +6,11 @@ import java.util.List; import java.util.HashMap; import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.os.Handler; +import android.os.Looper; -import android.util.Log; import com.android.billingclient.api.ConsumeParams; import com.android.billingclient.api.PendingPurchasesParams; @@ -43,6 +46,7 @@ public class InAppPurchaseModule extends ReactContextBaseJavaModule implements P public static final String TAG = NAME; private static final String E_CLIENT_IS_NOT_READY = "E_CLIENT_IS_NOT_READY"; + private static final String E_PRODUCT_DETAILS_NOT_FOUND = "E_PRODUCT_DETAILS_NOT_FOUND"; private static final String E_PRODUCT_IS_NOT_AVAILABLE = "E_PRODUCT_IS_NOT_AVAILABLE"; private static final String E_NO_PENDING_PURCHASE = "E_NO_PENDING_PURCHASE"; private static final String E_PURCHASE_CANCELED = "E_PURCHASE_CANCELED"; @@ -55,6 +59,8 @@ public class InAppPurchaseModule extends ReactContextBaseJavaModule implements P private final HashMap productDetailsHashMap = new HashMap<>(); private final BillingClient billingClient; private final GoogleApiAvailability googleApiAvailability; + private final AtomicBoolean isUserPurchasing = new AtomicBoolean(false); + private final Handler handler = new Handler(Looper.getMainLooper()); private Promise billingFlowPromise; @@ -72,12 +78,6 @@ public class InAppPurchaseModule extends ReactContextBaseJavaModule implements P googleApiAvailability = GoogleApiAvailability.getInstance(); } - @NonNull - @Override - public String getName() { - return NAME; - } - //--------------------------------- // React methods - These methods are exposed to React JS @@ -89,6 +89,17 @@ public String getName() { // //--------------------------------- + @ReactMethod(isBlockingSynchronousMethod = true) + public boolean isUserPurchasing() { + return isUserPurchasing.get(); + } + + + @NonNull + @Override + public String getName() { + return NAME; + } // Starts a connection with Google BillingClient @ReactMethod @@ -117,7 +128,47 @@ public void onBillingSetupFinished(@NonNull BillingResult billingResult) { @Override public void onBillingServiceDisconnected() { - promise.reject(E_CLIENT_IS_NOT_READY, "billing client service disconnected"); + promise.reject(E_CLIENT_IS_NOT_READY, "Billing client service disconnected!"); + } + }); + } + + @ReactMethod + public void getProductDetails(String productId, Promise promise) { + if (!isReady()) { + promise.reject(E_CLIENT_IS_NOT_READY, "Billing client is not ready, forgot to initialize?"); + return; + } + + + // try to fetch cached version of product details + if (productDetailsHashMap.containsKey(productId)) { + promise.resolve(this.productToJson(Objects.requireNonNull(productDetailsHashMap.get(productId)))); + return; + } + + // no cached product details + // fetch product details + ImmutableList productList = ImmutableList.of(QueryProductDetailsParams.Product.newBuilder() + .setProductId(productId) + .setProductType(BillingClient.ProductType.INAPP) + .build()); + + QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder() + .setProductList(productList) + .build(); + + billingClient.queryProductDetailsAsync(queryProductDetailsParams, (billingResult, list) -> { + if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && !list.isEmpty()) { + ProductDetails productDetails = list.get(0); + + // cache the product details + productDetailsHashMap.put(list.get(0).getProductId(), productDetails); + + // Launch the billing flow + promise.resolve(this.productToJson(productDetails)); + } else { + promise.reject(E_PRODUCT_IS_NOT_AVAILABLE, "Unable to load the product details with productId " + productId); } }); } @@ -125,7 +176,7 @@ public void onBillingServiceDisconnected() { @ReactMethod public void restorePurchases(Promise promise) { if (!isReady()) { - promise.reject(E_CLIENT_IS_NOT_READY, "billing client is not ready, forgot to initialize?"); + promise.reject(E_CLIENT_IS_NOT_READY, "Billing client is not ready, forgot to initialize?"); return; } @@ -152,7 +203,7 @@ public void restorePurchases(Promise promise) { @ReactMethod public void purchase(String productId, Promise promise) { if (!isReady()) { - promise.reject(E_CLIENT_IS_NOT_READY, "billingClient is not ready, forgot to initialize?"); + promise.reject(E_CLIENT_IS_NOT_READY, "Billing client is not ready, forgot to initialize?"); return; } @@ -162,34 +213,14 @@ public void purchase(String productId, Promise promise) { return; } - // no cached product details - // fetch product details - ImmutableList productList = ImmutableList.of(QueryProductDetailsParams.Product.newBuilder() - .setProductId(productId) - .setProductType(BillingClient.ProductType.INAPP) - .build()); - - QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder() - .setProductList(productList) - .build(); - - billingClient.queryProductDetailsAsync(queryProductDetailsParams, (billingResult, list) -> { - if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list.size() > 0) { - ProductDetails productDetails = list.get(0); - - // cache the product details - productDetailsHashMap.put(list.get(0).getProductId(), productDetails); - - // Launch the billing flow - launchBillingFlow(productDetails, promise); - } else { - Log.e(TAG, "Unable to load the product details with productId " + productId + " getResponseCode:" + billingResult.getResponseCode() + " list.size:" + list.size()); - promise.reject(E_PRODUCT_IS_NOT_AVAILABLE, "Unable to load the product details with productId " + productId); - } - }); + promise.reject(E_PRODUCT_DETAILS_NOT_FOUND, "Product is unavailable!"); } + //--------------------------------- + // Private methods + //--------------------------------- + // Consumes a purchase token, indicating that the product has been provided to the user. @ReactMethod public void finalizePurchase(String purchaseToken, Promise promise) { @@ -199,17 +230,11 @@ public void finalizePurchase(String purchaseToken, Promise promise) { if (billing.getResponseCode() == BillingClient.BillingResponseCode.OK) { promise.resolve(purchaseToken); } else { - promise.reject(E_FINISH_TRANSACTION_FAILED, "billing response code " + billing.getResponseCode()); + promise.reject(E_FINISH_TRANSACTION_FAILED, "Finalize purchase failed: code " + billing.getResponseCode()); } }); } - - //--------------------------------- - // Private methods - //--------------------------------- - - // This method gets called when purchases are updated. @Override public void onPurchasesUpdated(@NonNull BillingResult billing, @Nullable List list) { @@ -218,16 +243,22 @@ public void onPurchasesUpdated(@NonNull BillingResult billing, @Nullable List isUserPurchasing.set(false), 1000); + + // something went wrong with the purchase, reject the billingFlowPromise if (billing.getResponseCode() != BillingClient.BillingResponseCode.OK) { switch (billing.getResponseCode()) { case BillingClient.BillingResponseCode.USER_CANCELED: - rejectBillingFlowPromise(E_PURCHASE_CANCELED, "purchase canceled by users"); + rejectBillingFlowPromise(E_PURCHASE_CANCELED, "The purchase was canceled by the user."); break; case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED: - rejectBillingFlowPromise(E_ALREADY_PURCHASED, "item already owned by users"); + rejectBillingFlowPromise(E_ALREADY_PURCHASED, "The item is already owned."); + break; default: - rejectBillingFlowPromise(E_PURCHASE_FAILED, "billing response code " + billing.getResponseCode()); + rejectBillingFlowPromise(E_PURCHASE_FAILED, "An unexpected error occurred, code: " + billing.getResponseCode()); break; } return; @@ -236,7 +267,7 @@ public void onPurchasesUpdated(@NonNull BillingResult billing, @Nullable List { - static final String REACT_CLASS = "NativePayButton"; - - static final String DEFAULT_BUTTON_STYLE = "dark"; - private FrameLayout payButtonContainer; - private ThemedReactContext reactContext; - - @NonNull - @Override - public String getName() { - return REACT_CLASS; - } - - @Override - public @Nonnull FrameLayout createViewInstance(@Nonnull ThemedReactContext context) { - reactContext = context; - - payButtonContainer = new FrameLayout(context); - payButtonContainer.setLayoutParams(new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT - )); - - PayButton payButton = initWithPaymentButtonStyle(DEFAULT_BUTTON_STYLE, context); - payButtonContainer.addView(payButton); - - return payButtonContainer; - } - - @Override - @Nullable - public Map getExportedCustomDirectEventTypeConstants() { - return MapBuilder.of("onPress", MapBuilder.of("registrationName", "onPress")); - } - - private PayButton initWithPaymentButtonStyle(String style, ThemedReactContext context) { - int buttonTheme; - switch (style) { - case "light": - buttonTheme = ButtonConstants.ButtonTheme.LIGHT; - break; - default: - buttonTheme = ButtonConstants.ButtonTheme.DARK; - } - - PayButton payButton = new PayButton(context); - payButton.setId(R.id.pay_button_google_play); - payButton.initialize( - ButtonOptions.newBuilder() - .setButtonTheme(buttonTheme) - .setButtonType(ButtonConstants.ButtonType.PAY) - .setAllowedPaymentMethods(new JSONArray().toString()) - .build() - ); - - payButton.setOnClickListener(view -> ((ReactContext) view.getContext()).getJSModule(RCTEventEmitter.class).receiveEvent( - payButtonContainer.getId(), "onPress", - null)); - - return payButton; - - } - - @ReactProp(name = "buttonStyle") - public void setButtonStyle(FrameLayout layout, String value) { - PayButton oldPayButton = layout.findViewById(R.id.pay_button_google_play); - - if (oldPayButton != null) { - payButtonContainer.removeView(oldPayButton); - } - - PayButton payButton = initWithPaymentButtonStyle(value, reactContext); - payButtonContainer.addView(payButton); - - } -} diff --git a/android/app/src/main/java/libs/ui/UIPackage.java b/android/app/src/main/java/libs/ui/UIPackage.java index b9375236f..c18a21f7c 100644 --- a/android/app/src/main/java/libs/ui/UIPackage.java +++ b/android/app/src/main/java/libs/ui/UIPackage.java @@ -23,8 +23,7 @@ public List createNativeModules(ReactApplicationContext reactConte public List createViewManagers(ReactApplicationContext reactContext) { return Arrays.asList( new QRCodeModule(), - new BlurViewModule(), - new PayButtonModule() + new BlurViewModule() ); } } diff --git a/android/app/src/main/res/drawable/xaman_app_icon.png b/android/app/src/main/res/drawable/xaman_app_icon.png new file mode 100644 index 000000000..ce226723f Binary files /dev/null and b/android/app/src/main/res/drawable/xaman_app_icon.png differ diff --git a/android/app/src/main/res/xml/data_extraction_rules.xml b/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 000000000..fe61eb717 --- /dev/null +++ b/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/e2e/01_setup.feature b/e2e/01_setup.feature index 95fbb959d..d0e9273ef 100644 --- a/e2e/01_setup.feature +++ b/e2e/01_setup.feature @@ -14,11 +14,6 @@ Feature: Setup App Then I type my passcode Then I tap 'next-button' - Scenario: Setup Disclaimers - Given I should have 'disclaimers-setup-screen' - Given I should see 'disclaimer-content-view' - Then I agree all disclaimers - Scenario: Finish setup Given I should wait 5 sec to see 'agreement-setup-screen' Then I wait 5 sec for button 'confirm-button' to be enabled diff --git a/ios/Podfile b/ios/Podfile index f45cbb9d3..65c59652d 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,3 @@ -use_frameworks! :linkage => :static - # Resolve react_native_pods.rb with node to allow for hoisting require Pod::Executable.execute_command('node', ['-p', 'require.resolve( @@ -12,7 +10,6 @@ prepare_react_native_project! # no-ad-tracking Analytics subspec as we do not use it at all $RNFirebaseAnalyticsWithoutAdIdSupport = true -$RNFirebaseAsStaticFramework = true linkage = ENV['USE_FRAMEWORKS'] @@ -24,6 +21,15 @@ end target 'Xaman' do config = use_native_modules! + pod 'Firebase', :modular_headers => true + pod 'FirebaseCoreInternal', :modular_headers => true + pod 'GoogleUtilities', :modular_headers => true + pod 'FirebaseCore', :modular_headers => true + pod 'FirebaseInstallations', :modular_headers => true + pod 'FirebaseCoreExtension', :modular_headers => true + pod 'GoogleDataTransport', :modular_headers => true + pod 'nanopb', :modular_headers => true + use_react_native!( :path => config[:reactNativePath], :app_path => "#{Pod::Config.instance.installation_root}/.." diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 99986556a..78e3af73c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -2,9 +2,14 @@ PODS: - boost (1.83.0) - DoubleConversion (1.1.6) - FBLazyVector (0.74.2) + - Firebase (10.29.0): + - Firebase/Core (= 10.29.0) - Firebase/AnalyticsWithoutAdIdSupport (10.29.0): - Firebase/CoreOnly - FirebaseAnalytics/WithoutAdIdSupport (~> 10.29.0) + - Firebase/Core (10.29.0): + - Firebase/CoreOnly + - FirebaseAnalytics (~> 10.29.0) - Firebase/CoreOnly (10.29.0): - FirebaseCore (= 10.29.0) - Firebase/Crashlytics (10.29.0): @@ -13,6 +18,24 @@ PODS: - Firebase/Messaging (10.29.0): - Firebase/CoreOnly - FirebaseMessaging (~> 10.29.0) + - FirebaseAnalytics (10.29.0): + - FirebaseAnalytics/AdIdSupport (= 10.29.0) + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseAnalytics/AdIdSupport (10.29.0): + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleAppMeasurement (= 10.29.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30911.0, >= 2.30908.0) - FirebaseAnalytics/WithoutAdIdSupport (10.29.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) @@ -65,6 +88,20 @@ PODS: - PromisesSwift (~> 2.1) - fmt (9.1.0) - glog (0.3.5) + - GoogleAppMeasurement (10.29.0): + - GoogleAppMeasurement/AdIdSupport (= 10.29.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30911.0, >= 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (10.29.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.29.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30911.0, >= 2.30908.0) - GoogleAppMeasurement/WithoutAdIdSupport (10.29.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.11) - GoogleUtilities/MethodSwizzler (~> 7.11) @@ -75,6 +112,18 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities (7.13.3): + - GoogleUtilities/AppDelegateSwizzler (= 7.13.3) + - GoogleUtilities/Environment (= 7.13.3) + - GoogleUtilities/ISASwizzler (= 7.13.3) + - GoogleUtilities/Logger (= 7.13.3) + - GoogleUtilities/MethodSwizzler (= 7.13.3) + - GoogleUtilities/Network (= 7.13.3) + - "GoogleUtilities/NSData+zlib (= 7.13.3)" + - GoogleUtilities/Privacy (= 7.13.3) + - GoogleUtilities/Reachability (= 7.13.3) + - GoogleUtilities/SwizzlerTestHelpers (= 7.13.3) + - GoogleUtilities/UserDefaults (= 7.13.3) - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger @@ -83,6 +132,8 @@ PODS: - GoogleUtilities/Environment (7.13.3): - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/ISASwizzler (7.13.3): + - GoogleUtilities/Privacy - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Privacy @@ -100,6 +151,8 @@ PODS: - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - GoogleUtilities/SwizzlerTestHelpers (7.13.3): + - GoogleUtilities/MethodSwizzler - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - GoogleUtilities/Privacy @@ -1331,10 +1384,18 @@ DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) + - Firebase + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseInstallations - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`) + - GoogleDataTransport + - GoogleUtilities - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - Interactable (from `../node_modules/react-native-interactable`) + - nanopb - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`) @@ -1567,7 +1628,7 @@ SPEC CHECKSUMS: FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120 - glog: 69ef571f3de08433d766d614c73a9838a06bf7eb + glog: fdfdfe5479092de0c4bdbebedd9056951f092c4f GoogleAppMeasurement: f9de05ee17401e3355f68e8fc8b5064d429f5918 GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 @@ -1577,68 +1638,68 @@ SPEC CHECKSUMS: nanopb: 438bc412db1928dac798aa6fd75726007be04262 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740 + RCT-Folly: 02617c592a293bd6d418e0a88ff4ee1f88329b47 RCTDeprecation: b03c35057846b685b3ccadc9bfe43e349989cdb2 RCTRequired: 194626909cfa8d39ca6663138c417bc6c431648c RCTTypeSafety: 552aff5b8e8341660594db00e53ac889682bc120 React: a57fe42044fe6ed3e828f8867ce070a6c5872754 React-callinvoker: 6bedefb354a8848b534752417954caa3a5cf34f9 - React-Codegen: dd259bee42d0b3f511c054ad7d2c34b9c61ac7d5 + React-Codegen: 0952549a095f8f8cb2fb5def1733b6b232769b1c React-Core: 289ee3dfc1639bb9058c1e77427bb48169c26d7a React-CoreModules: eda5ce541a1f552158317abd90a5a0f6a4f8d6f7 React-cxxreact: 56bd17ccc6d4248116f7f95884ddb8c528379fb6 - React-debug: c15fd39ae1d2eee8619f923203f3852d12d32937 - React-Fabric: f6d33e4407e75ed616561257915a7a31b82c12d2 - React-FabricImage: 1b327a02a376e908d8e220114590febca05ccd56 - React-featureflags: 2e6c82cdada488928df15d293c9cb06bd5d74cea - React-graphics: 192b5905a8d8198281ea4d508bd320a3f34479df + React-debug: 164b8e302404d92d4bec39778a5e03bcb1b6eb08 + React-Fabric: 05620c36074e3ab397dd8f9db0deb6d3c38b4efa + React-FabricImage: 2a8a7f5729f5c44e32e6f58f7225ee1017ed0704 + React-featureflags: d97a6393993052e951e16a3b81206e22110be8d2 + React-graphics: ef07d701f4eb72ae6fca6ed0a7260a04f2a58dec React-hermes: 6ccc301ababfa17a9aad25a7e33faf325fd024b4 - React-ImageManager: 1a3ded3a51b6d8058a5733db1b3c4addc5c61770 - React-jserrorhandler: 2e1c8f5f806275d396ab9b30f7aaeccc89ddc659 + React-ImageManager: 00404bfe122626bc6493621f2a31ce802115a9b3 + React-jserrorhandler: 5e2632590a84363855b2083e6b3d501e93bc3f04 React-jsi: 828703c235f4eea1647897ee8030efdc6e8e9f14 React-jsiexecutor: 713d7bbef0a410cee5b3b78f73ed1fc16e177ba7 - React-jsinspector: a5866e1e0fb3ba64d5fec071956608eabae59781 - React-jsitracing: 34bc0fbc606f69e66f05286d94d5cd6cbf2ef562 + React-jsinspector: e1fa5325a47f34645195c63e3312ddb6a2efef5d + React-jsitracing: 0fa7f78d8fdda794667cb2e6f19c874c1cf31d7e React-logger: 29fa3e048f5f67fe396bc08af7606426d9bd7b5d - React-Mapbuffer: 86703e9e4f6522053568300827b436ccc01e1101 + React-Mapbuffer: bf56147c9775491e53122a94c423ac201417e326 react-native-camera: 3eae183c1d111103963f3dd913b65d01aef8110f react-native-netinfo: 076df4f9b07f6670acf4ce9a75aac8d34c2e2ccc react-native-sdk: f5e5134a8855b4b6d7e923c8332d236a5fd814b2 - React-nativeconfig: 5d452e509d6fbedc1522e21b566451fc673ac6b7 - React-NativeModulesApple: 6560431301ffdab8df6212cc8c8eff779396d8e0 + React-nativeconfig: 9f223cd321823afdecf59ed00861ab2d69ee0fc1 + React-NativeModulesApple: ff7efaff7098639db5631236cfd91d60abff04c0 React-perflogger: 32ed45d9cee02cf6639acae34251590dccd30994 React-RCTActionSheet: 19f967ddaea258182b56ef11437133b056ba2adf React-RCTAnimation: d7f4137fc44a08bba465267ea7cb1dbdb7c4ec87 React-RCTAppDelegate: 2b3f4d8009796af209a0d496e73276b743acee08 React-RCTBlob: c6c3e1e0251700b7bea036b893913f22e2b9cb47 - React-RCTFabric: bff98f06c6877ab795b6a71bec60ec726879eec7 + React-RCTFabric: 93a3ea55169d19294f07092013c1c9ea7a015c9b React-RCTImage: 40528ab74a4fef0f0e2ee797a074b26d120b6cc6 React-RCTLinking: 385b5beb96749aae9ae1606746e883e1c9f8a6a7 React-RCTNetwork: ffc9f05bd8fa5b3bce562199ba41235ad0af645c React-RCTSettings: 21914178bb65cb2c20c655ae1fb401617ae74618 React-RCTText: 7f8dba1a311e99f4de15bbace2350e805f33f024 React-RCTVibration: e4ccf673579d0d94a96b3a0b64492db08f8324d5 - React-rendererdebug: 1de9082085320466edb7c184bb23cdc614cc2293 - React-rncore: f8ce315afbe0835cc2324552e1019a956cb1ce36 - React-RuntimeApple: 025152efc25b08603a732fdc17e752973a17d785 - React-RuntimeCore: 965f409abab69cef1cd4e99a9c6748d9380456a5 + React-rendererdebug: ac70f40de137ce7bdbc55eaee60c467a215d9923 + React-rncore: edfff7a3f7f82ca1e0ba26978c6d84c7a8970dac + React-RuntimeApple: a0c98b75571aa5f44ddc7c6e9fd55803fa4db00f + React-RuntimeCore: 4b8db1fe2f3f4a3a5ecb22e1a419824e3e2cd7ef React-runtimeexecutor: 5961acc7a77b69f964e1645a5d6069e124ce6b37 - React-RuntimeHermes: 839b5525fa6171ed227f0ef351761b699cfc78ab - React-runtimescheduler: 1f0b3361460921edcbe9042ce690e5ca6dab9639 - React-utils: 5a40a1bb87d8c2d42a6bbbb7ead6d95785ca6042 - ReactCommon: e5866ad8f7487779853fa3fe33f1c075207c3262 + React-RuntimeHermes: c5825bfae4815fdf4e9e639340c3a986a491884c + React-runtimescheduler: 56b642bf605ba5afa500d35790928fc1d51565ad + React-utils: 4476b7fcbbd95cfd002f3e778616155241d86e31 + ReactCommon: ecad995f26e0d1e24061f60f4e5d74782f003f12 ReactNativeNavigation: 84cfcceb62947491beda20b96c5999c15ff5b959 RealmJS: a8c77aa700951e22403015390b33135f7ad4ace9 - RNFBAnalytics: cb56c22d47f8c22f33e6f033c40478d1347a5261 - RNFBApp: 10819a4f67d634fe34aa607c6d1e8a8aef019308 - RNFBCrashlytics: f465771d96a2eaf9f6104b30abb002cfe78fc0be - RNFBMessaging: 652aac21d91b8ddb85a5e25c0f04150ef60b5384 + RNFBAnalytics: 1d2ce0a73ad75bda19530f79eff05affbe450aee + RNFBApp: 2f83756f4d8be2859af5e2cf54130c20e380119d + RNFBCrashlytics: a845824a269968ebb7a4a6cf4772c4977125b5f6 + RNFBMessaging: 91592ea949854c66697757af648a2c07bcb583b9 RNTangemSdk: b4a4c91c617daca427a69cc7f784425c6789acd7 SocketRocket: abac6f5de4d4d62d24e11868d7a2f427e0ef940d TangemSdk: d782fc8c55cb56ffdfcaf7081422e02b19f18334 VeriffSDK: 9c4e31693dfe772af8553209c4a6543bffc75d24 - Yoga: 45564236f670899c9739b1581a12b00ead5d391f + Yoga: ae3c32c514802d30f687a04a6a35b348506d411f -PODFILE CHECKSUM: d570bd3dd1c9ff68463bd31cdebffd29014498b9 +PODFILE CHECKSUM: 02b3ced84ce14879cfec9b0703fe756147dd6237 -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.0 diff --git a/ios/Xaman.xcodeproj/project.pbxproj b/ios/Xaman.xcodeproj/project.pbxproj index 5852cea25..9d8459c90 100644 --- a/ios/Xaman.xcodeproj/project.pbxproj +++ b/ios/Xaman.xcodeproj/project.pbxproj @@ -10,8 +10,7 @@ 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 4E76AF7D78F0343A996A8CCE /* Pods_Xaman_XamanTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BFD60F0C13BA576045833BA5 /* Pods_Xaman_XamanTests.framework */; }; - 9F2B8C19DDAA855ECD2EF1A2 /* Pods_Xaman.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0C137C8E9CDF0648A92B4C5D /* Pods_Xaman.framework */; }; + 559A5D08536F62959115419B /* libPods-Xaman.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E970B0A9270522FB0AF008B0 /* libPods-Xaman.a */; }; A2021F6A28E591F300506772 /* CryptoTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2021F6928E591F300506772 /* CryptoTest.swift */; }; A2021F7528E5C53000506772 /* CipherTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A2021F7428E5C53000506772 /* CipherTest.m */; }; A2021F7728E6DF9700506772 /* VaultMangerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A2021F7628E6DF9700506772 /* VaultMangerTest.m */; }; @@ -24,8 +23,6 @@ A22A151E28C20014001A632E /* V2+AesGcm.m in Sources */ = {isa = PBXBuildFile; fileRef = A22A151D28C20014001A632E /* V2+AesGcm.m */; }; A22A152028C61E97001A632E /* Crypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = A22A151F28C61E97001A632E /* Crypto.swift */; }; A248635D28229379004633C9 /* BiometricModule.m in Sources */ = {isa = PBXBuildFile; fileRef = A248635C28229379004633C9 /* BiometricModule.m */; }; - A25A02522C13109300CE40B3 /* PayButton+View.m in Sources */ = {isa = PBXBuildFile; fileRef = A25A02512C13109300CE40B3 /* PayButton+View.m */; }; - A25A02552C131B4500CE40B3 /* PayButton+Manager.m in Sources */ = {isa = PBXBuildFile; fileRef = A25A02542C131B4500CE40B3 /* PayButton+Manager.m */; }; A2771FC229828E33008A95E3 /* security.txt in Resources */ = {isa = PBXBuildFile; fileRef = A2771FC129828E33008A95E3 /* security.txt */; }; A27996282BB5AFCE00998100 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = A27996272BB5AFCE00998100 /* PrivacyInfo.xcprivacy */; }; A2805CE224D1982400B5552E /* LocalNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = A2805CE124D1982400B5552E /* LocalNotification.m */; }; @@ -69,6 +66,7 @@ A2ED131C252F113A002AFC09 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A2ED131B252F113A002AFC09 /* StoreKit.framework */; }; A2F4F39528802A4600444302 /* UniqueIdProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = A2F4F39428802A4600444302 /* UniqueIdProvider.m */; }; A2F4F398288168CB00444302 /* Clipboard.m in Sources */ = {isa = PBXBuildFile; fileRef = A2F4F397288168CB00444302 /* Clipboard.m */; }; + CE2349EA1CF53D755A66BD4C /* libPods-Xaman-XamanTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AB54F464B5BCA7B88FD9167 /* libPods-Xaman-XamanTests.a */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -86,13 +84,13 @@ 00E356EE1AD99517003FC87E /* XamanTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = XamanTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 04A66EC2DDF90CD814AAB046 /* Pods-Xaman-XamanTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Xaman-XamanTests.release.xcconfig"; path = "Target Support Files/Pods-Xaman-XamanTests/Pods-Xaman-XamanTests.release.xcconfig"; sourceTree = ""; }; - 0C137C8E9CDF0648A92B4C5D /* Pods_Xaman.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Xaman.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07F961A680F5B00A75B9A /* Xaman.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Xaman.app; sourceTree = BUILT_PRODUCTS_DIR; }; 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Xaman/AppDelegate.h; sourceTree = ""; }; 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Xaman/AppDelegate.m; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Xaman/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Xaman/Info.plist; sourceTree = ""; }; 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Xaman/main.m; sourceTree = ""; }; + 7AB54F464B5BCA7B88FD9167 /* libPods-Xaman-XamanTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Xaman-XamanTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; A2021F6828E591F300506772 /* XamanTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "XamanTests-Bridging-Header.h"; sourceTree = ""; }; A2021F6928E591F300506772 /* CryptoTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoTest.swift; sourceTree = ""; }; A2021F7428E5C53000506772 /* CipherTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CipherTest.m; sourceTree = ""; }; @@ -113,10 +111,6 @@ A22A151F28C61E97001A632E /* Crypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Crypto.swift; sourceTree = ""; }; A248635C28229379004633C9 /* BiometricModule.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BiometricModule.m; sourceTree = ""; }; A248635E2822939E004633C9 /* BiometricModule.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BiometricModule.h; sourceTree = ""; }; - A25A02512C13109300CE40B3 /* PayButton+View.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "PayButton+View.m"; sourceTree = ""; }; - A25A02532C1310B900CE40B3 /* PayButton+View.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PayButton+View.h"; sourceTree = ""; }; - A25A02542C131B4500CE40B3 /* PayButton+Manager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "PayButton+Manager.m"; sourceTree = ""; }; - A25A02562C131B6800CE40B3 /* PayButton+Manager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "PayButton+Manager.h"; sourceTree = ""; }; A2771FC129828E33008A95E3 /* security.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = security.txt; sourceTree = ""; }; A27996272BB5AFCE00998100 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; A2805CE124D1982400B5552E /* LocalNotification.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocalNotification.m; sourceTree = ""; }; @@ -189,8 +183,8 @@ A2F4F396288168BC00444302 /* Clipboard.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Clipboard.h; sourceTree = ""; }; A2F4F397288168CB00444302 /* Clipboard.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Clipboard.m; sourceTree = ""; }; B712F110E060129D749043CD /* Pods-Xaman.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Xaman.debug.xcconfig"; path = "Target Support Files/Pods-Xaman/Pods-Xaman.debug.xcconfig"; sourceTree = ""; }; - BFD60F0C13BA576045833BA5 /* Pods_Xaman_XamanTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Xaman_XamanTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CA7812F0C935F7652256C087 /* Pods-Xaman-XamanTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Xaman-XamanTests.debug.xcconfig"; path = "Target Support Files/Pods-Xaman-XamanTests/Pods-Xaman-XamanTests.debug.xcconfig"; sourceTree = ""; }; + E970B0A9270522FB0AF008B0 /* libPods-Xaman.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Xaman.a"; sourceTree = BUILT_PRODUCTS_DIR; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; ED2971642150620600B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS12.0.sdk/System/Library/Frameworks/JavaScriptCore.framework; sourceTree = DEVELOPER_DIR; }; F35B10B5380FF4BC3DA7B7F5 /* Pods-Xaman.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Xaman.release.xcconfig"; path = "Target Support Files/Pods-Xaman/Pods-Xaman.release.xcconfig"; sourceTree = ""; }; @@ -201,7 +195,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 4E76AF7D78F0343A996A8CCE /* Pods_Xaman_XamanTests.framework in Frameworks */, + CE2349EA1CF53D755A66BD4C /* libPods-Xaman-XamanTests.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -210,7 +204,7 @@ buildActionMask = 2147483647; files = ( A2ED131C252F113A002AFC09 /* StoreKit.framework in Frameworks */, - 9F2B8C19DDAA855ECD2EF1A2 /* Pods_Xaman.framework in Frameworks */, + 559A5D08536F62959115419B /* libPods-Xaman.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -266,8 +260,8 @@ A2ED131B252F113A002AFC09 /* StoreKit.framework */, ED297162215061F000B7C4FE /* JavaScriptCore.framework */, ED2971642150620600B7C4FE /* JavaScriptCore.framework */, - 0C137C8E9CDF0648A92B4C5D /* Pods_Xaman.framework */, - BFD60F0C13BA576045833BA5 /* Pods_Xaman_XamanTests.framework */, + E970B0A9270522FB0AF008B0 /* libPods-Xaman.a */, + 7AB54F464B5BCA7B88FD9167 /* libPods-Xaman-XamanTests.a */, ); name = Frameworks; sourceTree = ""; @@ -329,17 +323,6 @@ path = Authentication; sourceTree = ""; }; - A25A02502C13106200CE40B3 /* PayButton */ = { - isa = PBXGroup; - children = ( - A25A02512C13109300CE40B3 /* PayButton+View.m */, - A25A02532C1310B900CE40B3 /* PayButton+View.h */, - A25A02542C131B4500CE40B3 /* PayButton+Manager.m */, - A25A02562C131B6800CE40B3 /* PayButton+Manager.h */, - ); - path = PayButton; - sourceTree = ""; - }; A266857724EEAD42000430B7 /* Notification */ = { isa = PBXGroup; children = ( @@ -479,7 +462,6 @@ A2AFF118243E79C30007005E /* UI */ = { isa = PBXGroup; children = ( - A25A02502C13106200CE40B3 /* PayButton */, A2A9F31C28859AB0009AE778 /* HapticFeedback */, A2A9F31B28859A98009AE778 /* BlurView */, A2A9F31A28859A6B009AE778 /* ToastView */, @@ -762,11 +744,9 @@ "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RealmJS/RealmJS.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/TangemSdk/TangemSdk.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", ); name = "[CP] Copy Pods Resources"; @@ -781,11 +761,9 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RealmJS.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TangemSdk.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; @@ -869,11 +847,9 @@ "${PODS_CONFIGURATION_BUILD_DIR}/GoogleUtilities/GoogleUtilities_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PromisesObjC/FBLPromises_Privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/PromisesSwift/Promises_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/RCT-Folly/RCT-Folly_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/React-Core/RCTI18nStrings.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/RealmJS/RealmJS.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/TangemSdk/TangemSdk.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/glog/glog_privacy.bundle", "${PODS_CONFIGURATION_BUILD_DIR}/nanopb/nanopb_Privacy.bundle", ); name = "[CP] Copy Pods Resources"; @@ -888,11 +864,9 @@ "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleUtilities_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/FBLPromises_Privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/Promises_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCT-Folly_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RCTI18nStrings.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/RealmJS.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/TangemSdk.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/glog_privacy.bundle", "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/nanopb_Privacy.bundle", ); runOnlyForDeploymentPostprocessing = 0; @@ -1003,7 +977,6 @@ A2AFF13B243E79C30007005E /* QRCode.m in Sources */, A2A9F32728A12C4C009AE778 /* RNCWKProcessPoolManager.m in Sources */, A2AFF13F243E79C30007005E /* CIColor+QRCode.m in Sources */, - A25A02522C13109300CE40B3 /* PayButton+View.m in Sources */, 13B07FC11A68108700A75B9A /* main.m in Sources */, A2D856C428802622003717BC /* HapticFeedback.m in Sources */, A2A9F31E28859AF4009AE778 /* BlurEffectWithAmount.m in Sources */, @@ -1015,7 +988,6 @@ A2AFF140243E79C30007005E /* NSString+Utils.m in Sources */, A22A151A28C0BECA001A632E /* Cipher.m in Sources */, A2BE295E2C0D028E00F66E10 /* InAppPurchase+TransactionObserver.m in Sources */, - A25A02552C131B4500CE40B3 /* PayButton+Manager.m in Sources */, A2A9F32928A12C4C009AE778 /* RNCWebView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1221,7 +1193,7 @@ INFOPLIST_FILE = Xaman/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 3.0.1; + MARKETING_VERSION = 3.1.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", @@ -1252,7 +1224,7 @@ INFOPLIST_FILE = Xaman/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 3.0.1; + MARKETING_VERSION = 3.1.0; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", diff --git a/ios/Xaman/AppDelegate.m b/ios/Xaman/AppDelegate.m index aee3055b2..daf71cece 100644 --- a/ios/Xaman/AppDelegate.m +++ b/ios/Xaman/AppDelegate.m @@ -34,15 +34,13 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [ReactNativeNavigation bootstrapWithBridge:bridge]; // init firebase app - if ([FIRApp defaultApp] == nil) { - [FIRApp configure]; - } + [FIRApp configure]; + // bootstrap local notification and Biometric module [LocalNotificationModule initialise]; [BiometricModule initialise]; - return YES; } diff --git a/ios/Xaman/Images.xcassets/Graphics/XamanAppIcon.imageset/AppIcon-1024.png b/ios/Xaman/Images.xcassets/Graphics/XamanAppIcon.imageset/AppIcon-1024.png new file mode 100644 index 000000000..ce226723f Binary files /dev/null and b/ios/Xaman/Images.xcassets/Graphics/XamanAppIcon.imageset/AppIcon-1024.png differ diff --git a/ios/Xaman/Images.xcassets/Graphics/XamanAppIcon.imageset/Contents.json b/ios/Xaman/Images.xcassets/Graphics/XamanAppIcon.imageset/Contents.json new file mode 100644 index 000000000..08b49de7b --- /dev/null +++ b/ios/Xaman/Images.xcassets/Graphics/XamanAppIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "AppIcon-1024.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Xaman/Info.plist b/ios/Xaman/Info.plist index 9929502a7..4b30a4d32 100644 --- a/ios/Xaman/Info.plist +++ b/ios/Xaman/Info.plist @@ -54,7 +54,7 @@ CFBundleVersion - 1 + 9 LSApplicationQueriesSchemes https diff --git a/ios/Xaman/Libs/Common/AppUpdate.m b/ios/Xaman/Libs/Common/AppUpdate.m index 1c358c7f3..1b588d2c4 100644 --- a/ios/Xaman/Libs/Common/AppUpdate.m +++ b/ios/Xaman/Libs/Common/AppUpdate.m @@ -32,7 +32,7 @@ @implementation AppUpdateModule return resolve(@NO); } - // if update vailable resolve the new version + // if update available resolve the new version BOOL isUpdateAvailable = [appStoreVersion compare:currentVersion options:NSNumericSearch] == NSOrderedDescending; if (isUpdateAvailable){ // new update is available diff --git a/ios/Xaman/Libs/Common/InAppPurchase/InAppPurchase+TransactionObserver.m b/ios/Xaman/Libs/Common/InAppPurchase/InAppPurchase+TransactionObserver.m index 873c40509..b8299dcd7 100644 --- a/ios/Xaman/Libs/Common/InAppPurchase/InAppPurchase+TransactionObserver.m +++ b/ios/Xaman/Libs/Common/InAppPurchase/InAppPurchase+TransactionObserver.m @@ -23,8 +23,8 @@ - (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedW [self callCompletionHandlerWithResult:[[NSMutableArray alloc] init]]; } -- (void) paymentQueue: (nonnull SKPaymentQueue *)queue - updatedTransactions: (nonnull NSArray *)transactions { +- (void)paymentQueue: (nonnull SKPaymentQueue *)queue + updatedTransactions: (nonnull NSArray *)transactions { NSMutableArray *result=[self transactionsToResult:transactions]; if([result count] > 0){ [self callCompletionHandlerWithResult:result]; @@ -42,20 +42,20 @@ - (void)callCompletionHandlerWithResult: (NSMutableArray *)result { - (NSMutableArray *)transactionsToResult: (nonnull NSArray *)transactions { NSMutableArray *result = [NSMutableArray arrayWithCapacity: [transactions count]]; - + [transactions enumerateObjectsUsingBlock: ^(SKPaymentTransaction *obj, NSUInteger idx, BOOL *stop) { NSMutableDictionary *paymentDictionary = [[NSMutableDictionary alloc] init]; - + NSString *productIdentifier = obj.payment.productIdentifier; NSString *transactionIdentifier = obj.transactionIdentifier; NSString *applicationUsername = obj.payment.applicationUsername; NSNumber *quantity = [NSNumber numberWithInteger:obj.payment.quantity]; - + [paymentDictionary setValue:productIdentifier forKey:@"productIdentifier"]; [paymentDictionary setValue:transactionIdentifier forKey:@"transactionIdentifier"]; [paymentDictionary setValue:applicationUsername forKey:@"applicationUsername"]; [paymentDictionary setValue:quantity forKey:@"quantity"]; - + switch(obj.transactionState){ // SUCCESS case SKPaymentTransactionStatePurchased: @@ -67,20 +67,49 @@ - (NSMutableArray *)transactionsToResult: (nonnull NSArray @interface InAppPurchaseModule : NSObject --(void)lunchBillingFlow:(SKProduct *) productDetails +-(void)launchBillingFlow:(SKProduct *) productDetails resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject; + (BOOL)isUserPurchasing; diff --git a/ios/Xaman/Libs/Common/InAppPurchase/InAppPurchase.m b/ios/Xaman/Libs/Common/InAppPurchase/InAppPurchase.m index 2ae1c4c48..80e8b31f9 100644 --- a/ios/Xaman/Libs/Common/InAppPurchase/InAppPurchase.m +++ b/ios/Xaman/Libs/Common/InAppPurchase/InAppPurchase.m @@ -1,3 +1,10 @@ +// +// InAppPurchase.m +// Xaman +// +// Created by XRPL Labs on 02/06/2024. +// + #import "InAppPurchase.h" #import @@ -16,6 +23,7 @@ @interface InAppPurchaseModule() { @implementation InAppPurchaseModule static NSString *const E_CLIENT_IS_NOT_READY = @"E_CLIENT_IS_NOT_READY"; +static NSString *const E_PRODUCT_DETAILS_NOT_FOUND = @"E_PRODUCT_DETAILS_NOT_FOUND"; static NSString *const E_PRODUCT_IS_NOT_AVAILABLE = @"E_PRODUCT_IS_NOT_AVAILABLE"; static NSString *const E_NO_PENDING_PURCHASE = @"E_NO_PENDING_PURCHASE"; static NSString *const E_PURCHASE_CANCELED = @"E_PURCHASE_CANCELED"; @@ -46,93 +54,137 @@ -(void) dealloc { #pragma mark JS methods /* - restorePurchases + getProductDetails purchase + restorePurchases finalizePurchase + isUserPurchasing */ -RCT_EXPORT_METHOD(purchase:(NSString *)productID resolver:(RCTPromiseResolveBlock)resolve +RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(isUserPurchasing) { + return @([InAppPurchaseModule isUserPurchasing]); +} + +RCT_EXPORT_METHOD(getProductDetails:(NSString *)productID resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - - // check if user can make the payment - if(![SKPaymentQueue canMakePayments]){ - reject(E_CLIENT_IS_NOT_READY, @"User cannot make payments due to parental controls", nil); - return; - } - - // try to find the product details in our cached details - [productDetailsMutableSet enumerateObjectsUsingBlock:^(SKProduct *product, BOOL *stop){ - if ([product.productIdentifier isEqualToString:productID]) { - // already have cached version of product details - // *** start the payment process - [self lunchBillingFlow:product resolver:resolve rejecter:reject]; - *stop = YES; - } - }]; - - // we couldn't find product details, lets try to fetch it __weak typeof(self) weakSelf = self; - // set the completion handler for product request - self->productRequestHandler.completionHandler = ^(BOOL didSucceed, NSArray *products, NSError *error) { - // we go the product details - if (didSucceed) { - __strong typeof(self) strongSelf = weakSelf; - SKProduct *product; - for(SKProduct *p in products) { - // cache the product details - [strongSelf->productDetailsMutableSet addObject:p]; - // find the product details we are looking for - if([p.productIdentifier isEqualToString:productID]){ - product = p; + dispatch_async(dispatch_get_main_queue(), ^{ + + // try to find the product details in our cached details + __block BOOL productFound = NO; + [self->productDetailsMutableSet enumerateObjectsUsingBlock:^(SKProduct *product, BOOL *stop){ + if ([product.productIdentifier isEqualToString:productID]) { + // already have cached version of product details + resolve([weakSelf productToJson:product]); + productFound = YES; + *stop = YES; + } + }]; + + if (!productFound) { + // we couldn't find product details, lets try to fetch it + // set the completion handler for product request + self->productRequestHandler.completionHandler = ^(BOOL didSucceed, NSArray *products, NSError *error) { + __strong typeof(self) strongSelf = weakSelf; + + // we go the product details + if (didSucceed) { + SKProduct *product = nil; + for(SKProduct *p in products) { + // cache the product details + [strongSelf->productDetailsMutableSet addObject:p]; + // find the product details we are looking for + if([p.productIdentifier isEqualToString:productID]){ + product = p; + } + } + if(product){ + resolve([strongSelf productToJson:product]); + }else{ + reject(E_PRODUCT_IS_NOT_AVAILABLE, [NSString stringWithFormat:@"product with id %@ not found!", productID], nil); + } + }else{ + reject(E_PRODUCT_IS_NOT_AVAILABLE, [NSString stringWithFormat:@"product with id %@ not found!", productID], nil); } + }; + + // start the product details request + [self->productRequestHandler startProductRequestForIdentifier:productID]; + } + }); +} + +RCT_EXPORT_METHOD(purchase:(NSString *)productID resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { + dispatch_async(dispatch_get_main_queue(), ^{ + // check if user can make the payment + if(![SKPaymentQueue canMakePayments]){ + reject(E_CLIENT_IS_NOT_READY, @"user cannot make payments due to parental controls", nil); + return; + } + + + __block SKProduct * productForPurchase; + // try to find the product details in our cached details + [self->productDetailsMutableSet enumerateObjectsUsingBlock:^(SKProduct *product, BOOL *stop){ + if ([product.productIdentifier isEqualToString:productID]) { + // already have cached version of product details + productForPurchase = product; + *stop = YES; } - // *** start the payment process - [strongSelf lunchBillingFlow:product resolver:resolve rejecter:reject]; - } else { - reject(E_CLIENT_IS_NOT_READY, @"User cannot make payments due to parental controls", nil); + }]; + + if(productForPurchase){ + // found the product details lets start the payment + [self launchBillingFlow:productForPurchase resolver:resolve rejecter:reject]; + }else{ + reject(E_PRODUCT_DETAILS_NOT_FOUND, [NSString stringWithFormat:@"product details with id %@ not found, make sure to run the getProductDetails method before purchase!", productID], nil); } - }; - // start the product details request - [self->productRequestHandler startProductRequestForIdentifier:productID]; + }); } RCT_EXPORT_METHOD(finalizePurchase:(NSString *)transactionIdentifier resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - if ([[SKPaymentQueue defaultQueue].transactions count] == 0) { - reject(E_FINISH_TRANSACTION_FAILED, @"transactions queue is empty.", nil); - return;; - } - - BOOL isTransactionFinished = NO; - for (SKPaymentTransaction *transaction in [[SKPaymentQueue defaultQueue] transactions]) { - if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier]){ - [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; - resolve(transactionIdentifier); - isTransactionFinished = YES; - break; + dispatch_async(dispatch_get_main_queue(), ^{ + if ([[SKPaymentQueue defaultQueue].transactions count] == 0) { + reject(E_FINISH_TRANSACTION_FAILED, @"transactions queue is empty.", nil); + return; } - } - - if(!isTransactionFinished){ - reject(E_FINISH_TRANSACTION_FAILED, [NSString stringWithFormat:@"Transaction with id %@ not found!", transactionIdentifier], nil); - } + + BOOL isTransactionFinished = NO; + for (SKPaymentTransaction *transaction in [[SKPaymentQueue defaultQueue] transactions]) { + if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier]){ + [[SKPaymentQueue defaultQueue] finishTransaction:transaction]; + resolve(transactionIdentifier); + isTransactionFinished = YES; + break; + } + } + + if(!isTransactionFinished){ + reject(E_FINISH_TRANSACTION_FAILED, [NSString stringWithFormat:@"transaction with id %@ not found!", transactionIdentifier], nil); + } + }); } RCT_EXPORT_METHOD(restorePurchases:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { - // set the completion handler for transaction updates - self->transactionObserver.completionHandler = ^(NSArray *result) { - resolve(result); - }; - - // start the transaction observer and restore completed transactions - [[SKPaymentQueue defaultQueue] addTransactionObserver:self->transactionObserver]; - [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; + dispatch_async(dispatch_get_main_queue(), ^{ + // set the completion handler for transaction updates + self->transactionObserver.completionHandler = ^(NSArray *result) { + resolve(result); + }; + + // start the transaction observer and restore completed transactions + [[SKPaymentQueue defaultQueue] addTransactionObserver:self->transactionObserver]; + [[SKPaymentQueue defaultQueue] restoreCompletedTransactions]; + }); } + #pragma mark Private --(void)lunchBillingFlow:(SKProduct *) product +-(void)launchBillingFlow:(SKProduct *) product resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject { // set the completion handler for transaction updates @@ -147,7 +199,23 @@ -(void)lunchBillingFlow:(SKProduct *) product [[SKPaymentQueue defaultQueue] addPayment:payment]; } -#pragma mark Public + + +-(NSDictionary *)productToJson:(SKProduct *)product { + NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init]; + [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4]; + [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle]; + [numberFormatter setLocale:product.priceLocale]; + + return @{ + @"title": [product localizedTitle], + @"description": [product localizedDescription], + @"price": [numberFormatter stringFromNumber:product.price], + @"productId": [product productIdentifier], + }; +} + +#pragma mark API +(BOOL)isUserPurchasing { for (SKPaymentTransaction* transaction in [[SKPaymentQueue defaultQueue] transactions]) { @@ -161,9 +229,5 @@ +(BOOL)isUserPurchasing { @end -#pragma mark API - - - diff --git a/ios/Xaman/Libs/Security/Authentication/Biometric/BiometricModule.m b/ios/Xaman/Libs/Security/Authentication/Biometric/BiometricModule.m index cb2e81a97..e4b7d874a 100644 --- a/ios/Xaman/Libs/Security/Authentication/Biometric/BiometricModule.m +++ b/ios/Xaman/Libs/Security/Authentication/Biometric/BiometricModule.m @@ -21,27 +21,29 @@ @implementation BiometricModule RCT_EXPORT_MODULE(); +(void)initialise { - // generate key if not exist - if(![SecurityProvider isKeyReady]){ - [SecurityProvider generateKey]; - } + dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // generate key if not exist + if(![SecurityProvider isKeyReady]){ + [SecurityProvider generateKey]; + } + }); } -// get normalize biomtery type +// get normalised biometry type - (NSString *)getBiometryType:(LAContext *)context { return context.biometryType == LABiometryTypeFaceID ? TYPE_BIOMETRIC_FACEID : TYPE_BIOMETRIC_TOUCHID; } -// get normalized sensore errors +// get normalised sensors errors -(NSString *) getSensorError: (LAContext *)context { NSError *error; - // can autherize, everything seems fine, no errros + // can authorise, everything seems fine, no errors if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) { return NULL; } - // biometrics are not available, reutrn error + // biometrics are not available, return error switch (error.code) { case LAErrorBiometryNotAvailable: return ERROR_NOT_AVAILABLE; @@ -60,7 +62,7 @@ -(NSString *) getSensorError: (LAContext *)context { LAContext *context = [[LAContext alloc] init]; NSString *error = [self getSensorError:context]; - // can authorize with biometrics + // can authorise with biometrics if(error == NULL){ // if sensor is available but the key is not generated @@ -82,17 +84,16 @@ -(NSString *) getSensorError: (LAContext *)context { rejecter:(RCTPromiseRejectBlock)reject) { dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - LAContext *context = [[LAContext alloc] init]; NSString *error = [self getSensorError:context]; - // pre check for authentication availabality + // pre check for authentication availability if(error != NULL){ reject(error, nil, nil); return; } - // set the athentication reason + // set the authentication reason context.localizedReason = reason; // remove fallback button context.localizedFallbackTitle = @""; @@ -103,7 +104,7 @@ -(NSString *) getSensorError: (LAContext *)context { // try to sign random bytes with private key // NOTE: this will trigger biometric authentication - NSString *result = [SecurityProvider signRandomBytes:context]; + NSString *result = [SecurityProvider signRandomBytesWithBackoff:context]; // we got the result set isUserAuthenticating to false @@ -115,7 +116,7 @@ -(NSString *) getSensorError: (LAContext *)context { return; } - // biometrics are not available, reutrn error + // biometrics are not available, return error if([result isEqual:ENCRYPTION_ERROR_CANCELLED]){ reject(ERROR_USER_CANCEL, nil, nil); return; diff --git a/ios/Xaman/Libs/Security/Authentication/Biometric/SecurityProvider.h b/ios/Xaman/Libs/Security/Authentication/Biometric/SecurityProvider.h index 238630bfd..4a8d53fa2 100644 --- a/ios/Xaman/Libs/Security/Authentication/Biometric/SecurityProvider.h +++ b/ios/Xaman/Libs/Security/Authentication/Biometric/SecurityProvider.h @@ -13,4 +13,5 @@ extern NSString *const ENCRYPTION_SUCCESS; + (void)generateKey; + (void)deleteInvalidKey; + (NSString *)signRandomBytes: (LAContext *)authentication_context; ++ (NSString *)signRandomBytesWithBackoff: (LAContext *)authentication_context; @end diff --git a/ios/Xaman/Libs/Security/Authentication/Biometric/SecurityProvider.m b/ios/Xaman/Libs/Security/Authentication/Biometric/SecurityProvider.m index f1e9b4f75..ffc24abc8 100644 --- a/ios/Xaman/Libs/Security/Authentication/Biometric/SecurityProvider.m +++ b/ios/Xaman/Libs/Security/Authentication/Biometric/SecurityProvider.m @@ -1,5 +1,7 @@ #import "SecurityProvider.h" +#import "Xaman-Swift.h" + // constants static NSString *const KEY_ALIAS = @"BiometricModuleKey"; @@ -36,18 +38,17 @@ + (void) generateKey { (id)kSecAttrAccessControl: (__bridge id)access} }; - if (access) { CFRelease(access); } - - SecKeyRef privateKeyRef = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &error); + CFRelease(access); + if (!privateKeyRef) { NSError *err = CFBridgingRelease(error); NSLog(@"BiometricModule: error when generating key OSStatus: %@", err.localizedDescription); return; } - // release private key refrence + // release private key reference CFRelease(privateKeyRef); } @@ -75,7 +76,6 @@ + (bool) isKeyReady { (id)kSecAttrApplicationTag: KEY_ALIAS, }; - OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)keyQuery, nil); return status == errSecSuccess; } @@ -91,29 +91,32 @@ + (NSString *) signRandomBytes:(LAContext *)authentication_context { (id)kSecUseAuthenticationContext: authentication_context }; - // fetch the private key refrence from keychian - // NOTE: this will not trigger the UI authenctiocation + // fetch the private key reference from keychain + // NOTE: this will NOT trigger the UI authentication SecKeyRef privateKeyRef; OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)keyQuery, (CFTypeRef *)&privateKeyRef); - // unable to fetch key refrence from keychain + // unable to fetch key reference from keychain if(status != errSecSuccess){ return ENCRYPTION_ERROR_FAILED; } - // generate random bytes to sign + // try to generate random bytes to sign with secure enclave NSMutableData *randomBytes = [NSMutableData dataWithLength:32]; int result = SecRandomCopyBytes(kSecRandomDefault, 32, randomBytes.mutableBytes); + // unable to create random bytes if (result != noErr) { + // clean up and return + CFRelease(privateKeyRef); return ENCRYPTION_ERROR_FAILED; } CFErrorRef error = NULL; CFDataRef dataRef = (__bridge CFDataRef)randomBytes; - // Scure Enclave only supports 256 - // NOTE: calling this method with privateKey ref will trigger UI authenctication + // Secure Enclave only supports 256 + // NOTE: calling this method with privateKey ref will trigger UI authentication CFDataRef resultRef = SecKeyCreateSignature(privateKeyRef, kSecKeyAlgorithmECDSASignatureMessageX962SHA256, @@ -121,25 +124,45 @@ + (NSString *) signRandomBytes:(LAContext *)authentication_context { &error); - - // release private key refrence + // release private key reference CFRelease(privateKeyRef); // error when creating signature if (!resultRef) { NSError *err = CFBridgingRelease(error); + // user cancelled the authentication if ([err.domain isEqual:@kLAErrorDomain] && (err.code == kLAErrorUserCancel || err.code == kLAErrorSystemCancel )) { return ENCRYPTION_ERROR_CANCELLED; } - // cannot encrypt the data for any reason + + // cannot encrypt the data for some reason return ENCRYPTION_ERROR_FAILED; } + // release the result reference + CFRelease(resultRef); + // everything seems fine return ENCRYPTION_SUCCESS; } ++ (NSString *)signRandomBytesWithBackoff:(LAContext *)authentication_context { + int maxRetries = 3; + int retryCount = 0; + + while (retryCount < maxRetries) { + NSString *result = [self signRandomBytes:authentication_context]; + if (![result isEqualToString:ENCRYPTION_ERROR_FAILED]) { + return result; + } + + retryCount++; + usleep((1 << retryCount) * 100000); // 2^retryCount * 100ms + } + return ENCRYPTION_ERROR_FAILED; +} + @end diff --git a/ios/Xaman/Libs/Security/Crypto/Crypto.h b/ios/Xaman/Libs/Security/Crypto/Crypto.h index a97379021..038c68180 100644 --- a/ios/Xaman/Libs/Security/Crypto/Crypto.h +++ b/ios/Xaman/Libs/Security/Crypto/Crypto.h @@ -1,3 +1,10 @@ +// +// Crypto.h +// Xaman +// +// Created by XRPL-Labs on 05/09/2022. +// + #import #import diff --git a/ios/Xaman/Libs/Security/Crypto/Crypto.m b/ios/Xaman/Libs/Security/Crypto/Crypto.m index 3f198ead9..4a6e57b0a 100644 --- a/ios/Xaman/Libs/Security/Crypto/Crypto.m +++ b/ios/Xaman/Libs/Security/Crypto/Crypto.m @@ -1,3 +1,10 @@ +// +// Crypto.m +// Xaman +// +// Created by XRPL-Labs on 05/09/2022. +// + #import #import #import diff --git a/ios/Xaman/Libs/Security/Crypto/Crypto.swift b/ios/Xaman/Libs/Security/Crypto/Crypto.swift index 51b1f3c02..03ca802af 100644 --- a/ios/Xaman/Libs/Security/Crypto/Crypto.swift +++ b/ios/Xaman/Libs/Security/Crypto/Crypto.swift @@ -1,5 +1,7 @@ // // Crypto.swift +// Xaman +// // Created by XRPL-Labs on 05/09/2022. // diff --git a/ios/Xaman/Libs/Security/Vault/Cipher/Cipher.h b/ios/Xaman/Libs/Security/Vault/Cipher/Cipher.h index e28a8864b..cb0486d8d 100644 --- a/ios/Xaman/Libs/Security/Vault/Cipher/Cipher.h +++ b/ios/Xaman/Libs/Security/Vault/Cipher/Cipher.h @@ -1,5 +1,6 @@ // // Cipher.h +// // Created by XRPL-Labs on 01/09/2022. // diff --git a/ios/Xaman/Libs/Security/Vault/Cipher/Cipher.m b/ios/Xaman/Libs/Security/Vault/Cipher/Cipher.m index 4f78b6ff9..d9c917039 100644 --- a/ios/Xaman/Libs/Security/Vault/Cipher/Cipher.m +++ b/ios/Xaman/Libs/Security/Vault/Cipher/Cipher.m @@ -1,5 +1,6 @@ // // Cipher.m +// // Created by XRPL-Labs on 01/09/2022. // @@ -10,7 +11,6 @@ @implementation Cipher - + (NSNumber *) getLatestCipherVersion { return [ CipherV2AesGcm getCipherVersion]; } diff --git a/ios/Xaman/Libs/Security/Vault/Cipher/V1+AesCbc.h b/ios/Xaman/Libs/Security/Vault/Cipher/V1+AesCbc.h index cb253e499..3dede5ee1 100644 --- a/ios/Xaman/Libs/Security/Vault/Cipher/V1+AesCbc.h +++ b/ios/Xaman/Libs/Security/Vault/Cipher/V1+AesCbc.h @@ -1,4 +1,12 @@ +// +// V1+AesCbc.h +// Xaman +// +// Created by XRPL-Labs on 01/09/2022. +// + #import + #import "Cipher.h" @interface CipherV1AesCbc : NSObject diff --git a/ios/Xaman/Libs/Security/Vault/Cipher/V1+AesCbc.m b/ios/Xaman/Libs/Security/Vault/Cipher/V1+AesCbc.m index de0ce7601..b40bbdadf 100644 --- a/ios/Xaman/Libs/Security/Vault/Cipher/V1+AesCbc.m +++ b/ios/Xaman/Libs/Security/Vault/Cipher/V1+AesCbc.m @@ -1,4 +1,13 @@ +// +// V1+AesCbc.m +// Xaman +// +// Created by XRPL-Labs on 01/09/2022. +// + + #import "V1+AesCbc.h" + #import "Xaman-Swift.h" #define CIPHER_VERSION @1 diff --git a/ios/Xaman/Libs/Security/Vault/Cipher/V2+AesGcm.h b/ios/Xaman/Libs/Security/Vault/Cipher/V2+AesGcm.h index c8ba48a4f..165096498 100644 --- a/ios/Xaman/Libs/Security/Vault/Cipher/V2+AesGcm.h +++ b/ios/Xaman/Libs/Security/Vault/Cipher/V2+AesGcm.h @@ -1,3 +1,10 @@ +// +// V2-AesGcm.h +// Xaman +// +// Created by XRPL-Labs on 01/09/2022. +// + #import #import "Cipher.h" diff --git a/ios/Xaman/Libs/Security/Vault/Cipher/V2+AesGcm.m b/ios/Xaman/Libs/Security/Vault/Cipher/V2+AesGcm.m index 378373a4c..65b7c1c8d 100644 --- a/ios/Xaman/Libs/Security/Vault/Cipher/V2+AesGcm.m +++ b/ios/Xaman/Libs/Security/Vault/Cipher/V2+AesGcm.m @@ -1,3 +1,10 @@ +// +// V2-AesGcm.m +// Xaman +// +// Created by XRPL-Labs on 01/09/2022. +// + #import "V2+AesGcm.h" #import "UniqueIdProvider.h" diff --git a/ios/Xaman/Libs/Security/Vault/VaultManager.m b/ios/Xaman/Libs/Security/Vault/VaultManager.m index 6266b2a2c..a9ac3adef 100644 --- a/ios/Xaman/Libs/Security/Vault/VaultManager.m +++ b/ios/Xaman/Libs/Security/Vault/VaultManager.m @@ -4,6 +4,7 @@ // // Created by XRPL-Labs on 01/09/2022. // + #import "VaultManager.h" #import diff --git a/ios/Xaman/Libs/UI/PayButton/PayButton+Manager.h b/ios/Xaman/Libs/UI/PayButton/PayButton+Manager.h deleted file mode 100644 index c88591e64..000000000 --- a/ios/Xaman/Libs/UI/PayButton/PayButton+Manager.h +++ /dev/null @@ -1,11 +0,0 @@ -// -// PayButton+Manager.h -// Xaman -// -// Created by XRPL Labs on 07/06/2024. -// - -#import - -@interface PayButtonManager : RCTViewManager -@end diff --git a/ios/Xaman/Libs/UI/PayButton/PayButton+Manager.m b/ios/Xaman/Libs/UI/PayButton/PayButton+Manager.m deleted file mode 100644 index 69404fa0a..000000000 --- a/ios/Xaman/Libs/UI/PayButton/PayButton+Manager.m +++ /dev/null @@ -1,28 +0,0 @@ -// -// PayButton+Manager.m -// Xaman -// -// Created by XRPL Labs on 07/06/2024. -// - -#import "PayButton+Manager.h" -#import "PayButton+View.h" - -@implementation PayButtonManager - -RCT_EXPORT_MODULE(NativePayButton) - -RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) -RCT_CUSTOM_VIEW_PROPERTY(buttonStyle, NSString, PayButtonView) -{ - if (json) { - [view initWithPaymentButtonStyle:[RCTConvert NSString:json]]; - } -} - -- (UIView *) view -{ - return [PayButtonView new]; -} - -@end diff --git a/ios/Xaman/Libs/UI/PayButton/PayButton+View.h b/ios/Xaman/Libs/UI/PayButton/PayButton+View.h deleted file mode 100644 index c0051eb9f..000000000 --- a/ios/Xaman/Libs/UI/PayButton/PayButton+View.h +++ /dev/null @@ -1,16 +0,0 @@ -// -// PaymentButtonView.h -// Xaman -// -// Created by XRPL Labs on 07/06/2024. -// - -#import -#import - -@interface PayButtonView : RCTView -- (void)initWithPaymentButtonStyle:(NSString *)style; -@property (nonatomic, readonly) PKPaymentButton *button; -@property (nonatomic, copy) RCTBubblingEventBlock onPress; -@property (strong, nonatomic) NSString *buttonStyle; -@end diff --git a/ios/Xaman/Libs/UI/PayButton/PayButton+View.m b/ios/Xaman/Libs/UI/PayButton/PayButton+View.m deleted file mode 100644 index 403570037..000000000 --- a/ios/Xaman/Libs/UI/PayButton/PayButton+View.m +++ /dev/null @@ -1,61 +0,0 @@ -// -// PayButtonView.m -// Xaman -// -// Created by XRPL Labs on 07/06/2024. -// - -#import - -#import "PayButton+View.h" - -NSString * const DEFAULT_BUTTON_STYLE = @"dark"; - -@implementation PayButtonView - -@synthesize buttonStyle = _buttonStyle; - -- (instancetype) init { - self = [super init]; - - [self initWithPaymentButtonStyle:DEFAULT_BUTTON_STYLE]; - - return self; -} - -- (void)initWithPaymentButtonStyle:(NSString *) value { - if (_buttonStyle != value) { - - PKPaymentButtonStyle style; - - if ([value isEqualToString: @"light"]) { - style = PKPaymentButtonStyleWhiteOutline; - } else { - style = PKPaymentButtonStyleBlack; - } - - - _button = [[PKPaymentButton alloc] initWithPaymentButtonType:PKPaymentButtonTypeInStore paymentButtonStyle:style]; - [_button addTarget:self action:@selector(touchUpInside:)forControlEvents:UIControlEventTouchUpInside]; - - [super setFrame:_button.frame]; - [self addSubview:_button]; - } - - - _buttonStyle = value; -} - -- (void)touchUpInside:(PKPaymentButton *)button { - if (self.onPress) { - self.onPress(nil); - } -} - -- (void)layoutSubviews -{ - [super layoutSubviews]; - _button.frame = self.bounds; -} - -@end diff --git a/ios/XamanTests/Info.plist b/ios/XamanTests/Info.plist index ba72822e8..0c590ff2f 100644 --- a/ios/XamanTests/Info.plist +++ b/ios/XamanTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1 + 9 diff --git a/package-lock.json b/package-lock.json index 67a1fee69..539421ba9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "xaman", - "version": "3.0.0", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "xaman", - "version": "3.0.0", + "version": "3.1.0", "hasInstallScript": true, "license": "SEE LICENSE IN ", "dependencies": { diff --git a/package.json b/package.json index 7307a1313..110c7c374 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xaman", - "version": "3.0.1", + "version": "3.1.0", "license": "SEE LICENSE IN ", "scripts": { "start": "node node_modules/react-native/cli.js start", diff --git a/src/__mocks__/react-native.ts b/src/__mocks__/react-native.ts index c05531347..23b5e50fd 100644 --- a/src/__mocks__/react-native.ts +++ b/src/__mocks__/react-native.ts @@ -105,4 +105,12 @@ ReactNative.NativeModules.VaultManagerModule = { ), }; +ReactNative.NativeModules.InAppPurchaseModule = { + isUserPurchasing: jest.fn((type) => false), +}; + +ReactNative.Animated.timing = () => ({ + start: (cb?: () => void) => (cb ? cb() : undefined), +}); + module.exports = ReactNative; diff --git a/src/app.tsx b/src/app.tsx index a8b3b11d3..d7e883645 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -100,6 +100,7 @@ class Application { if (message) { if ( message.indexOf('Realm file decryption failed') > -1 || + message.indexOf('Could not decrypt data') > -1 || message.indexOf('Could not decrypt bytes') > -1 ) { Alert.alert('Error', ErrorMessages.storageDecryptionFailed, [ @@ -192,13 +193,13 @@ class Application { .then(() => { resolve(); }) - .catch((e) => { - this.logger.error('initializeServices Error:', e); - reject(e); + .catch((error) => { + this.logger.error('initializeServices', error); + reject(error); }); - } catch (e) { - reject(e); - this.logger.error('initializeServices Error:', e); + } catch (error) { + reject(error); + this.logger.error('initializeServices', error); } }); }; @@ -215,9 +216,9 @@ class Application { } }); resolve(); - } catch (e: any) { - this.logger.error('reinstate Services Error:', e); - reject(e); + } catch (error) { + this.logger.error('reinstateServices', error); + reject(error); } }); }; diff --git a/src/common/constants/endpoints.ts b/src/common/constants/endpoints.ts index a02ab64dc..5a6074ea5 100644 --- a/src/common/constants/endpoints.ts +++ b/src/common/constants/endpoints.ts @@ -56,3 +56,8 @@ export enum WebLinks { // Other AppleStoreLink = `https://apps.apple.com/us/app/id1492302343`, } + +export enum ComplianceLinks { + PrivacyStatement = 'https://xrpl-labs.com/static/documents/XRPL-Labs-Privacy-Statement-V1.pdf', + TermsOfUse = 'https://xrpl-labs.com/static/documents/XRPL-Labs-Terms-of-Service-V1.pdf', +} diff --git a/src/common/constants/flags.ts b/src/common/constants/flags.ts index 441b7c5f5..51c782ae7 100644 --- a/src/common/constants/flags.ts +++ b/src/common/constants/flags.ts @@ -38,6 +38,9 @@ const LedgerEntryFlags = { lsfPassive: 0x00010000, lsfSell: 0x00020000, }, + [LedgerEntryTypes.URIToken]: { + lsfBurnable: 0x00000001, + }, }; export { LedgerEntryFlags }; diff --git a/src/common/constants/screens.ts b/src/common/constants/screens.ts index 654a93caa..f75f4ea23 100644 --- a/src/common/constants/screens.ts +++ b/src/common/constants/screens.ts @@ -8,7 +8,6 @@ const screens = { Passcode: 'app.Setup.Passcode', Biometric: 'app.Setup.Biometric', PushNotification: 'app.Setup.PushNotification', - Disclaimers: 'app.Setup.Disclaimers', Finish: 'app.Setup.Finish', }, TabBar: { @@ -43,6 +42,7 @@ const screens = { XAppBrowser: 'modal.XAppBrowser', DestinationPicker: 'modal.DestinationPicker', TransactionLoader: 'modal.TransactionLoader', + PurchaseProduct: 'modal.PurchaseProduct', }, Overlay: { SwitchAccount: 'overlay.SwitchAccount', @@ -71,7 +71,6 @@ const screens = { SwitchNetwork: 'overlay.SwitchNetwork', XAppInfo: 'overlay.XAppInfo', NetworkRailsSync: 'overlay.NetworkRailsSync', - PurchaseProduct: 'overlay.PurchaseProduct', }, Transaction: { Payment: 'app.Transaction.Payment', diff --git a/src/common/helpers/images.ts b/src/common/helpers/images.ts index ca7507bca..700e4d8ad 100644 --- a/src/common/helpers/images.ts +++ b/src/common/helpers/images.ts @@ -60,6 +60,7 @@ export const Images = { // Xaman XamanLogo: buildImageSource('XamanLogo', 'xaman_logo'), XamanLogoLight: buildImageSource('XamanLogoLight', 'xaman_logo_light'), + XamanAppIcon: buildImageSource('XamanAppIcon', 'xaman_app_icon'), // Icons IconTabBarScan: buildImageSource('IconTabBarScan', 'icon_tabbar_scan'), IconTabBarHome: buildImageSource('IconTabBarHome', 'icon_tabbar_home'), diff --git a/src/common/helpers/resolver.ts b/src/common/helpers/resolver.ts index 54f4a91b2..a8ef031e1 100644 --- a/src/common/helpers/resolver.ts +++ b/src/common/helpers/resolver.ts @@ -1,15 +1,26 @@ -import { memoize, has, get, assign } from 'lodash'; +/** + * AccountResolver is responsible for resolving account names and retrieving account information. + * It provides utility methods to look up account names based on the address and tag, + * as well as methods to fetch detailed account information including risk level and settings. + */ + +import { has, get, assign } from 'lodash'; import AccountRepository from '@store/repositories/account'; import ContactRepository from '@store/repositories/contact'; import LedgerService from '@services/LedgerService'; import BackendService from '@services/BackendService'; + import Amount from '@common/libs/ledger/parser/common/amount'; +import LRUCache from '@common/utils/cache'; +import LoggerService, { LoggerInstance } from '@services/LoggerService'; + +/* Types ==================================================================== */ export interface PayIDInfo { account: string; - tag: string; + tag: string | null; } export interface AccountNameType { @@ -22,228 +33,242 @@ export interface AccountNameType { export interface AccountInfoType { exist: boolean; - risk: 'ERROR' | 'UNKNOWS' | 'PROBABLE' | 'HIGH_PROBABILITY' | 'CONFIRMED'; + risk: 'ERROR' | 'UNKNOWN' | 'PROBABLE' | 'HIGH_PROBABILITY' | 'CONFIRMED'; requireDestinationTag: boolean; possibleExchange: boolean; disallowIncomingXRP: boolean; blackHole: boolean; } -const getAccountName = memoize( - (address: string, tag?: number, internal = false): Promise => { - return new Promise((resolve) => { - const notFound = { +/* Resolver ==================================================================== */ +class AccountResolver { + private static CacheSize = 300; + private cache: LRUCache | AccountNameType>; + private logger: LoggerInstance; + + constructor() { + this.cache = new LRUCache | AccountNameType>(AccountResolver.CacheSize); + this.logger = LoggerService.createLogger('AccountResolver'); + } + + private lookupresolveAccountName = async ( + address: string, + tag?: number, + internal = false, + ): Promise => { + const notFound: AccountNameType = { + address, + tag, + name: '', + source: '', + }; + + if (!address) { + return notFound; + } + + // Check in address book + try { + const contact = await ContactRepository.findOne({ address, - tag, - name: '', - source: '', - }; - - if (!address) { - resolve(notFound); - return; - } + destinationTag: `${tag ?? ''}`, + }); - // check address book - try { - const contact = ContactRepository.findOne({ + if (contact) { + return { address, - destinationTag: `${tag ?? ''}`, - }); - - if (contact) { - resolve({ - address, - tag, - name: contact.name, - source: 'contacts', - }); - return; - } - } catch { - // ignore + tag, + name: contact.name, + source: 'contacts', + }; } + } catch (error) { + this.logger.error('fetching contact:', error); + } - try { - // check in accounts list - const account = AccountRepository.findOne({ address }); - if (account) { - resolve({ - address, - tag, - name: account.label, - source: 'accounts', - }); - return; - } - } catch { - // ignore + // Check in accounts list + try { + const account = await AccountRepository.findOne({ address }); + if (account) { + return { + address, + tag, + name: account.label, + source: 'accounts', + }; } + } catch (error) { + this.logger.error('fetching account:', error); + } - // only lookup for local result - if (internal) { - resolve(notFound); - return; + // Only lookup for local results + if (internal) { + return notFound; + } + + // Check the backend + try { + const res = await BackendService.getAddressInfo(address); + if (res) { + return { + address, + tag, + name: res.name ?? undefined, + source: res.source?.replace('internal:', '').replace('.com', ''), + kycApproved: res.kycApproved, + }; } + } catch (error) { + this.logger.error('fetching info from API', error); + } - // check the backend - BackendService.getAddressInfo(address) - .then((res: any) => { - if (res) { - resolve({ - address, - tag, - name: res.name, - source: res.source?.replace('internal:', '').replace('.com', ''), - kycApproved: res.kycApproved, - }); - return; - } - resolve(notFound); - }) - .catch(() => { - resolve(notFound); - }); - }); - }, - (address: string, tag) => `${address}${tag ?? ''}`, -); - -const getAccountInfo = (address: string): Promise => { - /* eslint-disable-next-line */ - return new Promise(async (resolve, reject) => { + return notFound; + }; + + public setCache = (key: string, value: AccountNameType | Promise) => { + this.cache.set(key, value); + }; + + public getAccountName = async (address: string, tag?: number, internal = false): Promise => { if (!address) { - reject(); - return; + throw new Error('Address is required.'); + } + + const key = `${address}${tag ?? ''}`; + + const cachedValue = this.cache.get(key); + if (cachedValue) { + return cachedValue; } - const info = { + const resultPromise = (async () => { + const result = await this.lookupresolveAccountName(address, tag, internal); + this.cache.set(key, result); + return result; + })(); + + this.cache.set(key, resultPromise); // save the promise itself for subsequent calls + + return resultPromise; + }; + + getAccountInfo = async (address: string): Promise => { + if (!address) { + throw new Error('Address is required.'); + } + + const info: AccountInfoType = { exist: true, - risk: 'UNKNOWS', + risk: 'UNKNOWN', requireDestinationTag: false, possibleExchange: false, disallowIncomingXRP: false, blackHole: false, - } as AccountInfoType; + }; - try { - // get account risk level - const accountAdvisory = await BackendService.getAccountAdvisory(address); - - if (has(accountAdvisory, 'danger')) { - assign(info, { risk: accountAdvisory.danger }); - } else { - reject(); - return; - } + // get account risk level + const accountAdvisory = await BackendService.getAccountAdvisory(address); + + if (has(accountAdvisory, 'danger')) { + assign(info, { risk: accountAdvisory.danger }); + } else { + this.logger.error('account advisory risk level not found.'); + throw new Error('Account advisory risk level not found.'); + } - const accountInfo = await LedgerService.getAccountInfo(address); + const accountInfo = await LedgerService.getAccountInfo(address); - // account doesn't exist, no need to check account risk - if ('error' in accountInfo) { - if (get(accountInfo, 'error') === 'actNotFound') { - resolve(assign(info, { exist: false })); - return; - } - reject(); - return; + // account doesn't exist, no need to check account risk + if ('error' in accountInfo) { + if (get(accountInfo, 'error') === 'actNotFound') { + assign(info, { exist: false }); + return info; } + this.logger.error('fetching account info:', accountInfo); + throw new Error('Error fetching account info.'); + } - const { account_data, account_flags } = accountInfo; + const { account_data, account_flags } = accountInfo; - // if balance is more than 1m possibly exchange account - if (has(account_data, ['Balance'])) { - if (new Amount(account_data.Balance, true).dropsToNative().toNumber() > 1000000) { - assign(info, { possibleExchange: true }); - } + // if balance is more than 1m possibly exchange account + if (has(account_data, ['Balance'])) { + if (new Amount(account_data.Balance, true).dropsToNative().toNumber() > 1000000) { + assign(info, { possibleExchange: true }); } + } - // check for black hole - if (has(account_data, ['RegularKey'])) { - if ( - account_flags?.disableMasterKey && - ['rrrrrrrrrrrrrrrrrrrrrhoLvTp', 'rrrrrrrrrrrrrrrrrrrrBZbvji'].indexOf( - account_data.RegularKey ?? '', - ) > -1 - ) { - assign(info, { blackHole: true }); - } + // check for black hole + if (has(account_data, ['RegularKey'])) { + if ( + account_flags?.disableMasterKey && + ['rrrrrrrrrrrrrrrrrrrrrhoLvTp', 'rrrrrrrrrrrrrrrrrrrrBZbvji'].indexOf(account_data.RegularKey ?? '') > + -1 + ) { + assign(info, { blackHole: true }); } + } - // check for disallow incoming XRP - if (account_flags?.disallowIncomingXRP) { - assign(info, { disallowIncomingXRP: true }); - } + // check for disallow incoming XRP + if (account_flags?.disallowIncomingXRP) { + assign(info, { disallowIncomingXRP: true }); + } - if (get(accountAdvisory, 'force_dtag')) { - // first check on account advisory - assign(info, { requireDestinationTag: true, possibleExchange: true }); - } else if (account_flags?.requireDestinationTag) { - // check if account have the required destination tag flag set - assign(info, { requireDestinationTag: true, possibleExchange: true }); - } else { - // scan the most recent transactions of the account for the destination tags - const transactionsResp = await LedgerService.getTransactions(address, undefined, 200); - if ( - !('error' in transactionsResp) && - typeof transactionsResp.transactions !== 'undefined' && - transactionsResp.transactions && - transactionsResp.transactions.length > 0 - ) { - const incomingTXS = transactionsResp.transactions.filter((tx) => { - return tx.tx.Destination === address; - }); - - const incomingTxCountWithTag = incomingTXS.filter((tx) => { - return ( - typeof tx.tx.TransactionType === 'string' && - typeof tx.tx.DestinationTag !== 'undefined' && - Number(tx.tx.DestinationTag) > 9999 - ); - }).length; - - const senders = transactionsResp.transactions.map((tx) => { - return tx.tx.Account || ''; - }); - - const uniqueSenders = senders.filter((elem, pos) => { - return senders.indexOf(elem) === pos; - }).length; - - const percentageTag = (incomingTxCountWithTag / incomingTXS.length) * 100; - - if (uniqueSenders >= 10 && percentageTag > 50) { - assign(info, { requireDestinationTag: true, possibleExchange: true }); - } + if (get(accountAdvisory, 'force_dtag')) { + // first check on account advisory + assign(info, { requireDestinationTag: true, possibleExchange: true }); + } else if (account_flags?.requireDestinationTag) { + // check if account have the required destination tag flag set + assign(info, { requireDestinationTag: true, possibleExchange: true }); + } else { + // scan the most recent transactions of the account for the destination tags + const transactionsResp = await LedgerService.getTransactions(address, undefined, 200); + if ( + !('error' in transactionsResp) && + transactionsResp.transactions && + transactionsResp.transactions.length > 0 + ) { + const incomingTXS = transactionsResp.transactions.filter((tx) => tx.tx.Destination === address); + + const incomingTxCountWithTag = incomingTXS.filter( + (tx) => + typeof tx.tx.TransactionType === 'string' && + typeof tx.tx.DestinationTag !== 'undefined' && + Number(tx.tx.DestinationTag) > 9999, + ).length; + + const senders = transactionsResp.transactions.map((tx) => tx.tx.Account || ''); + + const uniqueSenders = new Set(senders).size; + + const percentageTag = (incomingTxCountWithTag / incomingTXS.length) * 100; + + if (uniqueSenders >= 10 && percentageTag > 50) { + assign(info, { requireDestinationTag: true, possibleExchange: true }); } } - - resolve(info); - } catch { - reject(); } - }); -}; - -const getPayIdInfo = (payId: string): Promise => { - return new Promise((resolve) => { - BackendService.lookup(payId) - .then((res: any) => { - if (res && res.error !== true) { - if (res.matches) { + + return info; + }; + + getPayIdInfo = (payId: string): Promise => { + return BackendService.lookup(payId) + .then((res) => { + if (res) { + if (Array.isArray(res.matches) && res.matches.length > 0) { const match = res.matches[0]; - return resolve({ + return { account: match.account, tag: match.tag, - }); + }; } } - return resolve(undefined); + return undefined; }) .catch(() => { - return resolve(undefined); + return undefined; }); - }); -}; + }; +} -export { getAccountName, getAccountInfo, getPayIdInfo }; +export default new AccountResolver(); diff --git a/src/common/libs/biometric.ts b/src/common/libs/biometric.ts index 65d696f21..d47b8b06e 100644 --- a/src/common/libs/biometric.ts +++ b/src/common/libs/biometric.ts @@ -20,6 +20,7 @@ export enum BiometricErrors { ERROR_NOT_MEET_SECURITY_REQUIREMENTS = 'NOT_MEET_SECURITY_REQUIREMENTS', ERROR_BIOMETRIC_HAS_BEEN_CHANGED = 'BIOMETRIC_HAS_BEEN_CHANGED', ERROR_UNABLE_REFRESH_AUTHENTICATION_KEY = 'UNABLE_REFRESH_AUTHENTICATION_KEY', + APP_NOT_ACTIVE = 'APP_NOT_ACTIVE', } /* Lib ==================================================================== */ diff --git a/src/common/libs/iap.ts b/src/common/libs/iap.ts index fdb6d045c..c838e83b3 100644 --- a/src/common/libs/iap.ts +++ b/src/common/libs/iap.ts @@ -32,12 +32,20 @@ interface GooglePlayPurchase { } interface AppStorePayment { + error?: string; transactionIdentifier: string; productIdentifier: string; quantity: number; applicationUsername?: string; } +export interface ProductDetails { + title: string; + description: string; + price: string; + productId: string; +} + export type InAppPurchaseReceipt = GooglePlayPurchase | AppStorePayment; /* Lib ==================================================================== */ const InAppPurchase = { @@ -50,12 +58,20 @@ const InAppPurchase = { } }, + /** + * Get product details + */ + getProductDetails: async (productId: string): Promise => { + await InAppPurchase.startConnectionIfAndroid(); + return InAppPurchaseModule.getProductDetails(productId); + }, + /** * Restore any old purchases */ restorePurchases: async () => { await InAppPurchase.startConnectionIfAndroid(); - return InAppPurchaseModule.restorePurchases(); + return InAppPurchaseModule.restorePurchases(); }, /** @@ -63,7 +79,7 @@ const InAppPurchase = { */ purchase: async (productId: string) => { await InAppPurchase.startConnectionIfAndroid(); - return InAppPurchaseModule.purchase(productId); + return InAppPurchaseModule.purchase(productId); }, /** @@ -74,6 +90,15 @@ const InAppPurchase = { return InAppPurchaseModule.finalizePurchase(transactionReceiptIdentifier); }, + /** + * Checks if the user is currently in the process of making a purchase. + * + * @returns {boolean} True if the user is making a purchase, otherwise false. + */ + isUserPurchasing: (): boolean => { + return InAppPurchaseModule.isUserPurchasing(); + }, + /** * Normalizes an error by returning its code, string representation, or a default error code. * @param {any} error - The error to be normalized. diff --git a/src/common/libs/ledger/factory/types.ts b/src/common/libs/ledger/factory/types.ts index ef1797f57..1db1a9afd 100644 --- a/src/common/libs/ledger/factory/types.ts +++ b/src/common/libs/ledger/factory/types.ts @@ -3,6 +3,21 @@ import { AccountModel } from '@store/models'; import { Account, AmountType, OperationActions } from '@common/libs/ledger/parser/types'; import { BalanceChanges } from '@common/libs/ledger/mixin/types'; +/** + * This type represents participants in a process, with optional stages defining their roles. + * + * @typedef {Object} Participants + * + * @property {Account} [start] - Represents the initial participant or state. + * @property {Account} [through] - Represents the intermediary participant or transitional state. + * @property {Account} [end] - Represents the final participant or ending state. + */ +export type Participants = { + start?: Account; + through?: Account; + end?: Account; +}; + /** * Represents the monetary status of a transaction. * @enum {string} @@ -57,11 +72,12 @@ export type MonetaryDetails = export type AssetDetails = | { type: AssetTypes.NFToken; - nfTokenId?: string; + nfTokenId: string; } | { type: AssetTypes.URIToken; - uriTokenId?: string; + uriTokenId: string; + owner: string; }; /** @@ -101,7 +117,7 @@ export abstract class ExplainerAbstract { * - through: {Account} The account through which the transaction passes * - end: {Account} The account where the transaction ends */ - abstract getParticipants(): { start?: Account; through?: Account; end?: Account }; + abstract getParticipants(): Participants; /** * Retrieves the monetary details. * diff --git a/src/common/libs/ledger/mixin/Mutations.mixin.ts b/src/common/libs/ledger/mixin/Mutations.mixin.ts index 6236fd04a..15795a468 100644 --- a/src/common/libs/ledger/mixin/Mutations.mixin.ts +++ b/src/common/libs/ledger/mixin/Mutations.mixin.ts @@ -108,12 +108,14 @@ export function MutationsMixin(Base: TBase) { if (feeIncludedBalanceIndex > -1) { const afterFee = new BigNumber(balanceChanges[feeIncludedBalanceIndex].value).minus(this.Fee!.value); + if (afterFee.isZero()) { // remove the item from balanceChanges balanceChanges.splice(feeIncludedBalanceIndex, 1); } else if ( afterFee.isNegative() && - this.TransactionType === TransactionTypes.NFTokenAcceptOffer && + (this.TransactionType === TransactionTypes.NFTokenAcceptOffer || + this.TransactionType === TransactionTypes.OfferCreate) && balanceChanges[feeIncludedBalanceIndex].action === OperationActions.DEC ) { // replace the action with Increase and positive the afterFee diff --git a/src/common/libs/ledger/objects/NFTokenOffer/NFTokenOffer.info.ts b/src/common/libs/ledger/objects/NFTokenOffer/NFTokenOffer.info.ts index 9cdb01037..48cc0aa54 100644 --- a/src/common/libs/ledger/objects/NFTokenOffer/NFTokenOffer.info.ts +++ b/src/common/libs/ledger/objects/NFTokenOffer/NFTokenOffer.info.ts @@ -107,7 +107,7 @@ class NFTokenOfferInfo extends ExplainerAbstract { } getAssetDetails(): AssetDetails[] { - return [{ type: AssetTypes.NFToken, nfTokenId: this.item.NFTokenID }]; + return [{ type: AssetTypes.NFToken, nfTokenId: this.item.NFTokenID! }]; } } diff --git a/src/common/libs/ledger/objects/URIToken/URIToken.class.ts b/src/common/libs/ledger/objects/URIToken/URIToken.class.ts new file mode 100644 index 000000000..bf9635b9b --- /dev/null +++ b/src/common/libs/ledger/objects/URIToken/URIToken.class.ts @@ -0,0 +1,64 @@ +import BaseLedgerObject from '@common/libs/ledger/objects/base'; + +import { Amount, AccountID, Hash256, UInt32, UInt64, Blob } from '@common/libs/ledger/parser/fields'; + +/* Types ==================================================================== */ +import { URIToken as URITokenEntry } from '@common/libs/ledger/types/ledger'; +import { LedgerEntryTypes } from '@common/libs/ledger/types/enums'; +import { FieldReturnType } from '@common/libs/ledger/parser/fields/types'; +import { RippleTime } from '@common/libs/ledger/parser/fields/codec'; + +/* Class ==================================================================== */ +class URIToken extends BaseLedgerObject { + public static Type = LedgerEntryTypes.URIToken as const; + public readonly Type = URIToken.Type; + + public static Fields = { + Owner: { type: AccountID }, + Destination: { type: AccountID }, + Amount: { type: Amount }, + Issuer: { type: AccountID }, + URI: { type: Blob }, + Digest: { type: Hash256 }, + + PreviousTxnID: { type: Hash256 }, + PreviousTxnLgrSeq: { type: UInt32 }, + LedgerCloseTime: { type: UInt32, codec: RippleTime }, + }; + + declare Amount: FieldReturnType; + declare Destination: FieldReturnType; + declare Issuer: FieldReturnType; + declare URI: FieldReturnType; + declare Digest: FieldReturnType; + + declare Owner: FieldReturnType; + declare OwnerNode: FieldReturnType; + declare PreviousTxnID: FieldReturnType; + declare PreviousTxnLgrSeq: FieldReturnType; + declare LedgerCloseTime: FieldReturnType; + + constructor(object: URITokenEntry) { + super(object); + + this.LedgerEntryType = LedgerEntryTypes.URIToken; + } + + get URITokenID(): string { + return this.Index; + } + + get Date(): string | undefined { + return this.LedgerCloseTime; + } + + /* + NOTE: as all classed and objects have Account field we normalize this object as the rest + */ + get Account() { + return this.Owner; + } +} + +/* Export ==================================================================== */ +export default URIToken; diff --git a/src/common/libs/ledger/objects/URIToken/URIToken.info.ts b/src/common/libs/ledger/objects/URIToken/URIToken.info.ts new file mode 100644 index 000000000..a7250077d --- /dev/null +++ b/src/common/libs/ledger/objects/URIToken/URIToken.info.ts @@ -0,0 +1,102 @@ +import Localize from '@locale'; + +import { AccountModel } from '@store/models'; + +import URIToken from '@common/libs/ledger/objects/URIToken/URIToken.class'; + +/* Types ==================================================================== */ +import { AssetDetails, AssetTypes, ExplainerAbstract, MonetaryStatus } from '@common/libs/ledger/factory/types'; +import { OperationActions } from '@common/libs/ledger/parser/types'; +import { NormalizeCurrencyCode } from '@common/utils/monetary'; + +/* Descriptor ==================================================================== */ +class URITokenInfo extends ExplainerAbstract { + constructor(item: URIToken, account: AccountModel) { + super(item, account); + } + + getEventsLabel(): string { + // Owner => The owner of the URI Token. + // Issuer => The issuer of the URI Token. + // Destination => The intended recipient of the URI Token. + if (this.item.Destination) { + // incoming offer + if (this.item.Destination === this.account.address) { + return Localize.t('events.uriTokenOfferedToYou'); + } + // outgoing offer + return Localize.t('events.sellURIToken'); + } + + return Localize.t('global.uritoken'); + } + + generateDescription(): string { + const content: string[] = []; + + if (typeof this.item.Destination !== 'undefined') { + if (this.item.Destination === this.account.address) { + content.push( + Localize.t('events.uriTokenOfferBuyExplain', { + address: this.item.Owner, + tokenID: this.item.Index, + amount: this.item.Amount!.value, + currency: NormalizeCurrencyCode(this.item.Amount!.currency), + }), + ); + } else { + content.push( + Localize.t('events.nftOfferSellExplain', { + address: this.item.Owner, + tokenID: this.item.Index, + amount: this.item.Amount!.value, + currency: NormalizeCurrencyCode(this.item.Amount!.currency), + }), + ); + } + } + + if (typeof this.item.Owner !== 'undefined') { + content.push(Localize.t('events.theUriTokenOwnerIs', { address: this.item.Owner })); + } + + if (typeof this.item.Destination !== 'undefined') { + content.push(Localize.t('events.thisUriTokenOfferCanOnlyBeAcceptedBy', { address: this.item.Destination })); + } + + return content.join('\n'); + } + + getParticipants() { + return { + start: { address: this.item.Owner, tag: undefined }, + end: { address: this.item.Destination, tag: undefined }, + }; + } + + getMonetaryDetails() { + const factor = []; + if (typeof this.item.Amount !== 'undefined') { + factor.push({ + ...this.item.Amount!, + effect: MonetaryStatus.POTENTIAL_EFFECT, + action: this.item.Owner === this.account.address ? OperationActions.INC : OperationActions.DEC, + }); + } + + return { + mutate: { + [OperationActions.INC]: [], + [OperationActions.DEC]: [], + }, + factor, + }; + } + + getAssetDetails(): AssetDetails[] { + return [{ type: AssetTypes.URIToken, uriTokenId: this.item.URITokenID, owner: this.item.Owner }]; + } +} + +/* Export ==================================================================== */ +export default URITokenInfo; diff --git a/src/common/libs/ledger/objects/URIToken/URIToken.validation.ts b/src/common/libs/ledger/objects/URIToken/URIToken.validation.ts new file mode 100644 index 000000000..bc355421e --- /dev/null +++ b/src/common/libs/ledger/objects/URIToken/URIToken.validation.ts @@ -0,0 +1,14 @@ +import URIToken from './URIToken.class'; + +/* Types ==================================================================== */ +import { ValidationType } from '@common/libs/ledger/factory/types'; + +/* Validation ==================================================================== */ +const URITokenValidation: ValidationType = (object: URIToken): Promise => { + return new Promise((resolve, reject) => { + reject(new Error(`Object type ${object.Type} does not contain validation!`)); + }); +}; + +/* Export ==================================================================== */ +export default URITokenValidation; diff --git a/src/common/libs/ledger/objects/URIToken/index.ts b/src/common/libs/ledger/objects/URIToken/index.ts new file mode 100644 index 000000000..5c42ac616 --- /dev/null +++ b/src/common/libs/ledger/objects/URIToken/index.ts @@ -0,0 +1,3 @@ +export { default as URIToken } from './URIToken.class'; +export { default as URITokenValidation } from './URIToken.validation'; +export { default as URITokenInfo } from './URIToken.info'; diff --git a/src/common/libs/ledger/objects/index.ts b/src/common/libs/ledger/objects/index.ts index 9afba1603..ab87644c3 100644 --- a/src/common/libs/ledger/objects/index.ts +++ b/src/common/libs/ledger/objects/index.ts @@ -6,3 +6,4 @@ export * from './NFTokenOffer'; export * from './Offer'; export * from './PayChannel'; export * from './Ticket'; +export * from './URIToken'; diff --git a/src/common/libs/ledger/objects/types.ts b/src/common/libs/ledger/objects/types.ts index 851ceda43..1c1818c15 100644 --- a/src/common/libs/ledger/objects/types.ts +++ b/src/common/libs/ledger/objects/types.ts @@ -1,3 +1,3 @@ -import { Offer, Escrow, Check, Ticket, PayChannel, NFTokenOffer } from '.'; +import { Offer, Escrow, Check, Ticket, PayChannel, NFTokenOffer, URIToken } from '.'; -export type LedgerObjects = Offer | Escrow | Check | Ticket | PayChannel | NFTokenOffer; +export type LedgerObjects = Offer | Escrow | Check | Ticket | PayChannel | NFTokenOffer | URIToken; diff --git a/src/common/libs/ledger/parser/fields/Blob.ts b/src/common/libs/ledger/parser/fields/Blob.ts index 8d35e3611..4273f1d96 100644 --- a/src/common/libs/ledger/parser/fields/Blob.ts +++ b/src/common/libs/ledger/parser/fields/Blob.ts @@ -2,7 +2,6 @@ export const Blob = { getter: (self: any, name: string) => { return (): string => { - // TODO: try to decode to UTF8 and if it was a valid string then return the string instead of blob return self[name]; }; }, @@ -15,7 +14,7 @@ export const Blob = { // TODO: valid we are setting hex value if (typeof value !== 'string') { - throw new Error(`field ${name} required type number, got ${typeof value}`); + throw new Error(`field ${name} required type string, got ${typeof value}`); } self[name] = value; diff --git a/src/common/libs/ledger/parser/fields/Hash128.ts b/src/common/libs/ledger/parser/fields/Hash128.ts index 7bbb9c58b..cdb349a6c 100644 --- a/src/common/libs/ledger/parser/fields/Hash128.ts +++ b/src/common/libs/ledger/parser/fields/Hash128.ts @@ -8,7 +8,7 @@ export const Hash128 = { setter: (self: any, name: string) => { return (value: string): void => { if (typeof value !== 'string') { - throw new Error(`field ${name} required type number, got ${typeof value}`); + throw new Error(`field ${name} required type string, got ${typeof value}`); } // TODO: add value check and validation diff --git a/src/common/libs/ledger/parser/fields/Hash256.ts b/src/common/libs/ledger/parser/fields/Hash256.ts index 6a8a4b2d1..014f5997c 100644 --- a/src/common/libs/ledger/parser/fields/Hash256.ts +++ b/src/common/libs/ledger/parser/fields/Hash256.ts @@ -8,7 +8,7 @@ export const Hash256 = { setter: (self: any, name: string) => { return (value: string): void => { if (typeof value !== 'string') { - throw new Error(`field ${name} required type number, got ${typeof value}`); + throw new Error(`field ${name} required type string, got ${typeof value}`); } // TODO: add value check and validation diff --git a/src/common/libs/ledger/parser/fields/codec/PriceDataSeries.ts b/src/common/libs/ledger/parser/fields/codec/PriceDataSeries.ts new file mode 100644 index 000000000..abcd4531a --- /dev/null +++ b/src/common/libs/ledger/parser/fields/codec/PriceDataSeries.ts @@ -0,0 +1,15 @@ +import { PriceData } from '@common/libs/ledger/types/common'; + +/* Codec ==================================================================== */ +export const PriceDataSeries = { + decode: (_self: any, value: { PriceData: PriceData }[]): PriceData[] => { + return value.map((s) => s.PriceData); + }, + encode: (_self: any, value: PriceData[]): { PriceData: PriceData }[] => { + return value.map((s) => { + return { + PriceData: s, + }; + }); + }, +}; diff --git a/src/common/libs/ledger/parser/fields/codec/index.ts b/src/common/libs/ledger/parser/fields/codec/index.ts index 25feafa44..e0636a2c4 100644 --- a/src/common/libs/ledger/parser/fields/codec/index.ts +++ b/src/common/libs/ledger/parser/fields/codec/index.ts @@ -6,6 +6,8 @@ import { Memos } from './Memos'; import { RippleTime } from './RippleTime'; import { Signers } from './Signers'; import { SignerEntries } from './SignerEntries'; +import { PriceDataSeries } from './PriceDataSeries'; + import { TransferRate } from './TransferRate'; import { TransferFee } from './TransferFee'; import { TradingFee } from './TradingFee'; @@ -26,4 +28,5 @@ export { TradingFee, AuthAccounts, Amounts, + PriceDataSeries, }; diff --git a/src/common/libs/ledger/transactions/genuine/NFTokenAcceptOffer/NFTokenAcceptOffer.info.ts b/src/common/libs/ledger/transactions/genuine/NFTokenAcceptOffer/NFTokenAcceptOffer.info.ts index ee98ee286..b2fefd499 100644 --- a/src/common/libs/ledger/transactions/genuine/NFTokenAcceptOffer/NFTokenAcceptOffer.info.ts +++ b/src/common/libs/ledger/transactions/genuine/NFTokenAcceptOffer/NFTokenAcceptOffer.info.ts @@ -88,7 +88,7 @@ class NFTokenAcceptOfferInfo extends ExplainerAbstract; + + constructor(tx?: TransactionJson, meta?: TransactionMetadata) { + super(tx, meta); + + // set transaction type + this.TransactionType = OracleDelete.Type; + } +} + +/* Export ==================================================================== */ +export default OracleDelete; diff --git a/src/common/libs/ledger/transactions/genuine/OracleDelete/OracleDelete.info.ts b/src/common/libs/ledger/transactions/genuine/OracleDelete/OracleDelete.info.ts new file mode 100644 index 000000000..a28b6d2d9 --- /dev/null +++ b/src/common/libs/ledger/transactions/genuine/OracleDelete/OracleDelete.info.ts @@ -0,0 +1,40 @@ +import Localize from '@locale'; + +import { AccountModel } from '@store/models'; + +import OracleDelete from './OracleDelete.class'; + +/* Types ==================================================================== */ +import { MutationsMixinType } from '@common/libs/ledger/mixin/types'; +import { ExplainerAbstract } from '@common/libs/ledger/factory/types'; + +/* Descriptor ==================================================================== */ +class OracleDeleteInfo extends ExplainerAbstract { + constructor(item: OracleDelete & MutationsMixinType, account: AccountModel) { + super(item, account); + } + + getEventsLabel(): string { + return Localize.t('events.oracleDelete'); + } + + generateDescription(): string { + return `This is an ${this.item.Type} transaction`; + } + + getParticipants() { + return { + start: { address: this.item.Account, tag: this.item.SourceTag }, + }; + } + + getMonetaryDetails() { + return { + mutate: this.item.BalanceChange(this.account.address), + factor: undefined, + }; + } +} + +/* Export ==================================================================== */ +export default OracleDeleteInfo; diff --git a/src/common/libs/ledger/transactions/genuine/OracleDelete/OracleDelete.validation.ts b/src/common/libs/ledger/transactions/genuine/OracleDelete/OracleDelete.validation.ts new file mode 100644 index 000000000..eb338adea --- /dev/null +++ b/src/common/libs/ledger/transactions/genuine/OracleDelete/OracleDelete.validation.ts @@ -0,0 +1,15 @@ +import AccountSet from './OracleDelete.class'; + +/* Types ==================================================================== */ +import { ValidationType } from '@common/libs/ledger/factory/types'; + +/* Validation ==================================================================== */ +const OracleDeleteValidation: ValidationType = (): Promise => { + // TODO: add validation + return new Promise((resolve) => { + resolve(); + }); +}; + +/* Export ==================================================================== */ +export default OracleDeleteValidation; diff --git a/src/common/libs/ledger/transactions/genuine/OracleDelete/index.ts b/src/common/libs/ledger/transactions/genuine/OracleDelete/index.ts new file mode 100644 index 000000000..d8dbea30f --- /dev/null +++ b/src/common/libs/ledger/transactions/genuine/OracleDelete/index.ts @@ -0,0 +1,3 @@ +export { default as OracleDelete } from './OracleDelete.class'; +export { default as OracleDeleteValidation } from './OracleDelete.validation'; +export { default as OracleDeleteInfo } from './OracleDelete.info'; diff --git a/src/common/libs/ledger/transactions/genuine/OracleSet/OracleSet.class.ts b/src/common/libs/ledger/transactions/genuine/OracleSet/OracleSet.class.ts new file mode 100644 index 000000000..054694fa0 --- /dev/null +++ b/src/common/libs/ledger/transactions/genuine/OracleSet/OracleSet.class.ts @@ -0,0 +1,45 @@ +/** + * OracleSet transaction + */ + +import BaseGenuineTransaction from '@common/libs/ledger/transactions/genuine/base'; + +import { Blob, STArray, UInt32 } from '@common/libs/ledger/parser/fields'; +import { Hex, PriceDataSeries } from '@common/libs/ledger/parser/fields/codec'; + +/* Types ==================================================================== */ +import { TransactionJson, TransactionMetadata } from '@common/libs/ledger/types/transaction'; +import { TransactionTypes } from '@common/libs/ledger/types/enums'; +import { FieldConfig, FieldReturnType } from '@common/libs/ledger/parser/fields/types'; + +/* Class ==================================================================== */ +class OracleSet extends BaseGenuineTransaction { + public static Type = TransactionTypes.OracleSet as const; + public readonly Type = OracleSet.Type; + + public static Fields: { [key: string]: FieldConfig } = { + OracleDocumentID: { type: UInt32 }, + Provider: { type: Blob, codec: Hex }, + URI: { type: Blob }, + LastUpdateTime: { type: UInt32 }, + AssetClass: { type: Blob, codec: Hex }, + PriceDataSeries: { type: STArray, codec: PriceDataSeries }, + }; + + declare OracleDocumentID: FieldReturnType; + declare Provider: FieldReturnType; + declare URI: FieldReturnType; + declare LastUpdateTime: FieldReturnType; + declare AssetClass: FieldReturnType; + declare PriceDataSeries: FieldReturnType; + + constructor(tx?: TransactionJson, meta?: TransactionMetadata) { + super(tx, meta); + + // set transaction type + this.TransactionType = OracleSet.Type; + } +} + +/* Export ==================================================================== */ +export default OracleSet; diff --git a/src/common/libs/ledger/transactions/genuine/OracleSet/OracleSet.info.ts b/src/common/libs/ledger/transactions/genuine/OracleSet/OracleSet.info.ts new file mode 100644 index 000000000..58bcf0df0 --- /dev/null +++ b/src/common/libs/ledger/transactions/genuine/OracleSet/OracleSet.info.ts @@ -0,0 +1,40 @@ +import Localize from '@locale'; + +import { AccountModel } from '@store/models'; + +import OracleSet from './OracleSet.class'; + +/* Types ==================================================================== */ +import { MutationsMixinType } from '@common/libs/ledger/mixin/types'; +import { ExplainerAbstract } from '@common/libs/ledger/factory/types'; + +/* Descriptor ==================================================================== */ +class OracleSetInfo extends ExplainerAbstract { + constructor(item: OracleSet & MutationsMixinType, account: AccountModel) { + super(item, account); + } + + getEventsLabel(): string { + return Localize.t('events.oracleSet'); + } + + generateDescription(): string { + return `This is an ${this.item.Type} transaction`; + } + + getParticipants() { + return { + start: { address: this.item.Account, tag: this.item.SourceTag }, + }; + } + + getMonetaryDetails() { + return { + mutate: this.item.BalanceChange(this.account.address), + factor: undefined, + }; + } +} + +/* Export ==================================================================== */ +export default OracleSetInfo; diff --git a/src/common/libs/ledger/transactions/genuine/OracleSet/OracleSet.validation.ts b/src/common/libs/ledger/transactions/genuine/OracleSet/OracleSet.validation.ts new file mode 100644 index 000000000..96ef71c54 --- /dev/null +++ b/src/common/libs/ledger/transactions/genuine/OracleSet/OracleSet.validation.ts @@ -0,0 +1,15 @@ +import OracleSet from './OracleSet.class'; + +/* Types ==================================================================== */ +import { ValidationType } from '@common/libs/ledger/factory/types'; + +/* Validation ==================================================================== */ +const OracleSetValidation: ValidationType = (): Promise => { + // TODO: add validation + return new Promise((resolve) => { + resolve(); + }); +}; + +/* Export ==================================================================== */ +export default OracleSetValidation; diff --git a/src/common/libs/ledger/transactions/genuine/OracleSet/index.ts b/src/common/libs/ledger/transactions/genuine/OracleSet/index.ts new file mode 100644 index 000000000..1ac126954 --- /dev/null +++ b/src/common/libs/ledger/transactions/genuine/OracleSet/index.ts @@ -0,0 +1,3 @@ +export { default as OracleSet } from './OracleSet.class'; +export { default as OracleSetValidation } from './OracleSet.validation'; +export { default as OracleSetInfo } from './OracleSet.info'; diff --git a/src/common/libs/ledger/transactions/genuine/URITokenBurn/URITokenBurn.info.ts b/src/common/libs/ledger/transactions/genuine/URITokenBurn/URITokenBurn.info.ts index c6e4bafc4..a2a7c908b 100644 --- a/src/common/libs/ledger/transactions/genuine/URITokenBurn/URITokenBurn.info.ts +++ b/src/common/libs/ledger/transactions/genuine/URITokenBurn/URITokenBurn.info.ts @@ -6,7 +6,7 @@ import URITokenBurn from './URITokenBurn.class'; /* Types ==================================================================== */ import { MutationsMixinType } from '@common/libs/ledger/mixin/types'; -import { ExplainerAbstract } from '@common/libs/ledger/factory/types'; +import { AssetDetails, AssetTypes, ExplainerAbstract } from '@common/libs/ledger/factory/types'; /* Descriptor ==================================================================== */ class URITokenBurnInfo extends ExplainerAbstract { @@ -36,6 +36,10 @@ class URITokenBurnInfo extends ExplainerAbstract { @@ -47,6 +47,10 @@ class URITokenBuyInfo extends ExplainerAbstract ], }; } + + getAssetDetails(): AssetDetails[] { + return [{ type: AssetTypes.URIToken, owner: this.item.Account, uriTokenId: this.item.URITokenID! }]; + } } /* Export ==================================================================== */ diff --git a/src/common/libs/ledger/transactions/genuine/URITokenCancelSellOffer/URITokenCancelSellOffer.info.ts b/src/common/libs/ledger/transactions/genuine/URITokenCancelSellOffer/URITokenCancelSellOffer.info.ts index ef9513b94..dad003ee7 100644 --- a/src/common/libs/ledger/transactions/genuine/URITokenCancelSellOffer/URITokenCancelSellOffer.info.ts +++ b/src/common/libs/ledger/transactions/genuine/URITokenCancelSellOffer/URITokenCancelSellOffer.info.ts @@ -3,7 +3,7 @@ import URITokenCancelSellOffer from './URITokenCancelSellOffer.class'; /* Types ==================================================================== */ import { MutationsMixinType } from '@common/libs/ledger/mixin/types'; -import { ExplainerAbstract } from '@common/libs/ledger/factory/types'; +import { AssetDetails, AssetTypes, ExplainerAbstract } from '@common/libs/ledger/factory/types'; import { AccountModel } from '@store/models'; /* Descriptor ==================================================================== */ @@ -35,6 +35,10 @@ class URITokenCancelSellOfferInfo extends ExplainerAbstract { @@ -46,6 +47,7 @@ class URITokenCreateSellOfferInfo extends ExplainerAbstract { + if (node.CreatedNode && node.CreatedNode.LedgerEntryType === 'URIToken') { + const { LedgerIndex } = node.CreatedNode; + if (LedgerIndex) { + uriTokenId = LedgerIndex; + } + } + }); + + return uriTokenId; + } } /* Export ==================================================================== */ diff --git a/src/common/libs/ledger/transactions/genuine/URITokenMint/URITokenMint.info.ts b/src/common/libs/ledger/transactions/genuine/URITokenMint/URITokenMint.info.ts index c56c2e8d7..ab1ba50b9 100644 --- a/src/common/libs/ledger/transactions/genuine/URITokenMint/URITokenMint.info.ts +++ b/src/common/libs/ledger/transactions/genuine/URITokenMint/URITokenMint.info.ts @@ -7,7 +7,8 @@ import URITokenMint from './URITokenMint.class'; /* Types ==================================================================== */ import { MutationsMixinType } from '@common/libs/ledger/mixin/types'; -import { ExplainerAbstract } from '@common/libs/ledger/factory/types'; +import { AssetDetails, AssetTypes, ExplainerAbstract, MonetaryStatus } from '@common/libs/ledger/factory/types'; +import { OperationActions } from '@common/libs/ledger/parser/types'; /* Descriptor ==================================================================== */ class URITokenMintInfo extends ExplainerAbstract { @@ -16,52 +17,71 @@ class URITokenMintInfo extends ExplainerAbstract { issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', value: '0.012136', }); - // expect(instance.TakerPaid()).toStrictEqual({ - // action: OperationActions.INC, - // currency: 'BTC', - // issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', - // value: '0.01257026', - // }); expect(instance.TakerGets).toStrictEqual({ currency: 'XRP', value: '500', }); - // expect(instance.TakerGot()).toStrictEqual({ - // action: OperationActions.DEC, - // currency: 'XRP', - // value: '500', - // }); }); it('Should return right parsed values for executed order IOU->XRP', () => { @@ -62,22 +51,11 @@ describe('OfferCreate tx', () => { currency: 'XRP', value: '484.553386', }); - // expect(instance.TakerPaid()).toStrictEqual({ - // action: OperationActions.INC, - // currency: 'XRP', - // value: '501.44754', - // }); expect(instance.TakerGets).toStrictEqual({ currency: 'BTC', issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', value: '0.01257', }); - // expect(instance.TakerGot()).toStrictEqual({ - // action: OperationActions.DEC, - // currency: 'BTC', - // issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', - // value: '0.01257026', - // }); }); it('Should set right values XRP->IOU ', () => { @@ -132,80 +110,52 @@ describe('OfferCreate tx', () => { currency: 'XRP', value: '100', }); - // expect(instance.TakerGot('rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ')).toStrictEqual({ - // action: OperationActions.DEC, - // currency: '534F4C4F00000000000000000000000000000000', - // issuer: 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz', - // value: '38.46538462', - // }); expect(instance.TakerPays).toStrictEqual({ currency: '534F4C4F00000000000000000000000000000000', issuer: 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz', value: '38.076', }); - // expect(instance.TakerPaid('rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ')).toStrictEqual({ - // action: OperationActions.INC, - // currency: 'XRP', - // value: '100', - // }); }); it('Should return zero for taker got and taker paid if order cancelled or killed', () => { const { tx, meta }: any = offerCreateTemplates.XRPIOUCANCELED; const instance = new OfferCreate(tx, meta); - // expect(instance.Executed).toBe(true); expect(instance.OfferSequence).toBe(61160755); - expect(instance.TakerGets).toStrictEqual({ currency: 'XRP', value: '50', }); - // expect(instance.TakerGot('rQamE9ddZiRZLKRAAzwGKboQ8rQHgesjEs')).toStrictEqual({ - // currency: 'XRP', - // value: '0', - // }); expect(instance.TakerPays).toStrictEqual({ currency: 'CSC', issuer: 'rCSCManTZ8ME9EoLrSHHYKW8PPwWMgkwr', value: '11616.66671104', }); - // expect(instance.TakerPaid('rQamE9ddZiRZLKRAAzwGKboQ8rQHgesjEs')).toStrictEqual({ - // currency: 'CSC', - // issuer: 'rCSCManTZ8ME9EoLrSHHYKW8PPwWMgkwr', - // value: '0', - // }); }); }); describe('Info', () => { - const Mixed = MutationsMixin(OfferCreate); - const { tx, meta }: any = offerCreateTemplates.IOUXRP; - const instance = new Mixed(tx, meta); - const info = new OfferCreateInfo(instance, { address: tx.Account } as any); + describe('IOU->XRP', () => { + const Mixed = MutationsMixin(OfferCreate); + const { tx, meta }: any = offerCreateTemplates.IOUXRP; + const instance = new Mixed(tx, meta); + const info = new OfferCreateInfo(instance, { address: tx.Account } as any); - describe('generateDescription()', () => { it('should return the expected description', () => { const expectedDescription = `rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ offered to pay 0.01257 BTC in order to receive 484.553386 XRP${'\n'}The exchange rate for this offer is 0.000025941414017897298 BTC/XRP${'\n'}The transaction will also cancel rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ 's existing offer #112${'\n'}The transaction offer ID is: EF963D9313AA45E85610598797D1A65E${'\n'}The offer expires at Monday, September 20, 2021 11:38 AM unless canceled or consumed before then.`; expect(info.generateDescription()).toEqual(expectedDescription); }); - }); - describe('getEventsLabel()', () => { it('should return the expected label', () => { expect(info.getEventsLabel()).toEqual(Localize.t('events.exchangedAssets')); }); - }); - describe('getParticipants()', () => { it('should return the expected participants', () => { expect(info.getParticipants()).toStrictEqual({ start: { address: 'rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ', tag: undefined }, }); }); - }); - describe('getMonetaryDetails()', () => { it('should return the expected monetary details', () => { expect(info.getMonetaryDetails()).toStrictEqual({ factor: [ @@ -243,6 +193,234 @@ describe('OfferCreate tx', () => { }); }); }); + describe('XRP->IOU', () => { + const Mixed = MutationsMixin(OfferCreate); + const { tx, meta }: any = offerCreateTemplates.XRPIOU; + const instance = new Mixed(tx, meta); + const info = new OfferCreateInfo(instance, { address: tx.Account } as any); + + it('should return the expected description', () => { + const expectedDescription = `rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ offered to pay 500 XRP in order to receive 0.012136 BTC +The exchange rate for this offer is 0.000024271999999999997 BTC/XRP +The transaction will also cancel rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ 's existing offer #94 +The transaction offer ID is: EF963D9313AA45E85610598797D1A65E`; + expect(info.generateDescription()).toEqual(expectedDescription); + }); + + it('should return the expected label', () => { + expect(info.getEventsLabel()).toEqual(Localize.t('events.exchangedAssets')); + }); + + it('should return the expected participants', () => { + expect(info.getParticipants()).toStrictEqual({ + start: { address: 'rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ', tag: undefined }, + }); + }); + + it('should return the expected monetary details', () => { + expect(info.getMonetaryDetails()).toStrictEqual({ + factor: [ + { + action: 'INC', + currency: 'BTC', + effect: 'POTENTIAL_EFFECT', + issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + value: '0.012136', + }, + { + action: 'DEC', + effect: 'POTENTIAL_EFFECT', + value: '500', + currency: 'XRP', + }, + ], + mutate: { + DEC: [ + { + action: 'DEC', + currency: 'XRP', + value: '500', + }, + ], + INC: [ + { + action: 'INC', + currency: 'BTC', + issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', + value: '0.01257026', + }, + ], + }, + }); + }); + }); + describe('XRP->IOU [Different Owner]', () => { + const Mixed = MutationsMixin(OfferCreate); + const { tx, meta }: any = offerCreateTemplates.XRPIOUDifferentOwner; + const instance = new Mixed(tx, meta); + const info = new OfferCreateInfo(instance, { address: 'rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ' } as any); + + it('should return the expected description', () => { + const expectedDescription = `rsTQsbTfRkqgUxxs8BToD3VdnENaha9UcY offered to pay 100 XRP in order to receive 38.076 SOLO +The exchange rate for this offer is 0.38076 SOLO/XRP +The transaction will also cancel rsTQsbTfRkqgUxxs8BToD3VdnENaha9UcY 's existing offer #56270334`; + expect(info.generateDescription()).toEqual(expectedDescription); + }); + + it('should return the expected label', () => { + expect(info.getEventsLabel()).toEqual(Localize.t('events.exchangedAssets')); + }); + + it('should return the expected participants', () => { + expect(info.getParticipants()).toStrictEqual({ + start: { address: tx.Account, tag: undefined }, + }); + }); + + it('should return the expected monetary details', () => { + expect(info.getMonetaryDetails()).toStrictEqual({ + factor: [ + { + action: 'INC', + effect: 'POTENTIAL_EFFECT', + currency: '534F4C4F00000000000000000000000000000000', + issuer: 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz', + value: '38.076', + }, + { + action: 'DEC', + effect: 'POTENTIAL_EFFECT', + value: '100', + currency: 'XRP', + }, + ], + mutate: { + DEC: [ + { + action: 'DEC', + currency: '534F4C4F00000000000000000000000000000000', + issuer: 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz', + value: '38.46538462', + }, + ], + INC: [ + { + action: 'INC', + currency: 'XRP', + value: '100', + }, + ], + }, + }); + }); + }); + describe('XRP->IOU [Canceled]', () => { + const Mixed = MutationsMixin(OfferCreate); + const { tx, meta }: any = offerCreateTemplates.XRPIOUCANCELED; + const instance = new Mixed(tx, meta); + const info = new OfferCreateInfo(instance, { address: tx.Account } as any); + + it('should return the expected description', () => { + const expectedDescription = `rQamE9ddZiRZLKRAAzwGKboQ8rQHgesjEs offered to pay 50 XRP in order to receive 11616.66671104 CSC +The exchange rate for this offer is 232.3333342208 CSC/XRP +The transaction will also cancel rQamE9ddZiRZLKRAAzwGKboQ8rQHgesjEs 's existing offer #61160755`; + expect(info.generateDescription()).toEqual(expectedDescription); + }); + + it('should return the expected label', () => { + expect(info.getEventsLabel()).toEqual(Localize.t('events.createOffer')); + }); + + it('should return the expected participants', () => { + expect(info.getParticipants()).toStrictEqual({ + start: { address: tx.Account, tag: undefined }, + }); + }); + + it('should return the expected monetary details', () => { + expect(info.getMonetaryDetails()).toStrictEqual({ + factor: [ + { + action: 'INC', + effect: 'POTENTIAL_EFFECT', + currency: 'CSC', + issuer: 'rCSCManTZ8ME9EoLrSHHYKW8PPwWMgkwr', + value: '11616.66671104', + }, + { + action: 'DEC', + effect: 'POTENTIAL_EFFECT', + value: '50', + currency: 'XRP', + }, + ], + mutate: { + DEC: [], + INC: [], + }, + }); + }); + }); + describe('XRP->IOU [Fee]', () => { + const Mixed = MutationsMixin(OfferCreate); + const { tx, meta }: any = offerCreateTemplates.XRPIOUWithFeeConflict; + const instance = new Mixed(tx, meta); + const info = new OfferCreateInfo(instance, { address: tx.Account } as any); + + it('should return the expected description', () => { + const expectedDescription = `rBeSemGtLaHLZXcK1WxutWR339L9ZUz4gh offered to pay 0.6742741104345 SOLO in order to receive 0.000001 XRP +The exchange rate for this offer is 674274.1104345 SOLO/XRP +The transaction will also cancel rBeSemGtLaHLZXcK1WxutWR339L9ZUz4gh 's existing offer #91995334`; + expect(info.generateDescription()).toEqual(expectedDescription); + }); + + it('should return the expected label', () => { + expect(info.getEventsLabel()).toEqual(Localize.t('events.exchangedAssets')); + }); + + it('should return the expected participants', () => { + expect(info.getParticipants()).toStrictEqual({ + start: { address: tx.Account, tag: undefined }, + }); + }); + + it('should return the expected monetary details', () => { + expect(info.getMonetaryDetails()).toStrictEqual({ + factor: [ + { + action: 'INC', + effect: 'POTENTIAL_EFFECT', + currency: 'XRP', + value: '0.000001', + }, + { + action: 'DEC', + effect: 'POTENTIAL_EFFECT', + currency: '534F4C4F00000000000000000000000000000000', + issuer: 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz', + value: '0.6742741104345', + }, + ], + mutate: { + DEC: [ + { + action: 'DEC', + currency: '534F4C4F00000000000000000000000000000000', + issuer: 'rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz', + value: '0.000006', + }, + ], + INC: [ + { + action: 'INC', + currency: 'XRP', + value: '0.000001', + }, + ], + }, + }); + }); + }); }); describe('Validation', () => {}); diff --git a/src/common/libs/ledger/transactions/genuine/__tests__/oracleSet.test.ts b/src/common/libs/ledger/transactions/genuine/__tests__/oracleSet.test.ts new file mode 100644 index 000000000..ef5a4321f --- /dev/null +++ b/src/common/libs/ledger/transactions/genuine/__tests__/oracleSet.test.ts @@ -0,0 +1,80 @@ +/* eslint-disable max-len */ + +import Localize from '@locale'; + +import { MutationsMixin } from '@common/libs/ledger/mixin'; + +import { OracleSet, OracleSetInfo } from '../OracleSet'; +import oracleSetTemplate from './fixtures/OracleSetTx.json'; + +jest.mock('@services/NetworkService'); + +describe('OracleSet tx', () => { + describe('Class', () => { + it('Should set tx type if not set', () => { + const instance = new OracleSet(); + expect(instance.TransactionType).toBe('OracleSet'); + expect(instance.Type).toBe('OracleSet'); + }); + + it('Should return right parsed values', () => { + const { tx, meta }: any = oracleSetTemplate; + const instance = new OracleSet(tx, meta); + + expect(instance.AssetClass).toBe('currency'); + expect(instance.Provider).toBe('provider'); + expect(instance.LastUpdateTime).toBe(1729759640); + expect(instance.OracleDocumentID).toBe(1337); + expect(instance.PriceDataSeries).toMatchObject([ + { + AssetPrice: '2e4', + BaseAsset: 'XRP', + QuoteAsset: 'USD', + Scale: 3, + }, + ]); + }); + }); + + describe('Info', () => { + const { tx, meta }: any = oracleSetTemplate; + const Mixed = MutationsMixin(OracleSet); + const instance = new Mixed(tx, meta); + const info = new OracleSetInfo(instance, {} as any); + + describe('generateDescription()', () => { + it('should return the expected description', () => { + const expectedDescription = 'This is an OracleSet transaction'; + expect(info.generateDescription()).toEqual(expectedDescription); + }); + }); + + describe('getEventsLabel()', () => { + it('should return the expected label', () => { + expect(info.getEventsLabel()).toEqual(Localize.t('events.oracleSet')); + }); + }); + + describe('getParticipants()', () => { + it('should return the expected participants', () => { + expect(info.getParticipants()).toStrictEqual({ + start: { address: 'rsYxnKtb8JBzfG4hp6sVF3WiVNw2broUFo', tag: undefined }, + }); + }); + }); + + describe('getMonetaryDetails()', () => { + it('should return the expected monetary details', () => { + expect(info.getMonetaryDetails()).toStrictEqual({ + mutate: { + DEC: [], + INC: [], + }, + factor: undefined, + }); + }); + }); + }); + + describe('Validation', () => {}); +}); diff --git a/src/common/libs/ledger/transactions/genuine/__tests__/orcleDelete.test.ts b/src/common/libs/ledger/transactions/genuine/__tests__/orcleDelete.test.ts new file mode 100644 index 000000000..9d12aa221 --- /dev/null +++ b/src/common/libs/ledger/transactions/genuine/__tests__/orcleDelete.test.ts @@ -0,0 +1,69 @@ +/* eslint-disable max-len */ + +import Localize from '@locale'; + +import { MutationsMixin } from '@common/libs/ledger/mixin'; + +import { OracleDelete, OracleDeleteInfo } from '../OracleDelete'; +import oracleDeleteTemplate from './fixtures/OracleDeleteTx.json'; + +jest.mock('@services/NetworkService'); + +describe('OracleDelete tx', () => { + describe('Class', () => { + it('Should set tx type if not set', () => { + const instance = new OracleDelete(); + expect(instance.TransactionType).toBe('OracleDelete'); + expect(instance.Type).toBe('OracleDelete'); + }); + + it('Should return right parsed values', () => { + const { tx, meta }: any = oracleDeleteTemplate; + const instance = new OracleDelete(tx, meta); + + expect(instance.OracleDocumentID).toBe(1337); + }); + }); + + describe('Info', () => { + const { tx, meta }: any = oracleDeleteTemplate; + const Mixed = MutationsMixin(OracleDelete); + const instance = new Mixed(tx, meta); + const info = new OracleDeleteInfo(instance, {} as any); + + describe('generateDescription()', () => { + it('should return the expected description', () => { + const expectedDescription = 'This is an OracleDelete transaction'; + expect(info.generateDescription()).toEqual(expectedDescription); + }); + }); + + describe('getEventsLabel()', () => { + it('should return the expected label', () => { + expect(info.getEventsLabel()).toEqual(Localize.t('events.oracleDelete')); + }); + }); + + describe('getParticipants()', () => { + it('should return the expected participants', () => { + expect(info.getParticipants()).toStrictEqual({ + start: { address: 'rsYxnKtb8JBzfG4hp6sVF3WiVNw2broUFo', tag: undefined }, + }); + }); + }); + + describe('getMonetaryDetails()', () => { + it('should return the expected monetary details', () => { + expect(info.getMonetaryDetails()).toStrictEqual({ + mutate: { + DEC: [], + INC: [], + }, + factor: undefined, + }); + }); + }); + }); + + describe('Validation', () => {}); +}); diff --git a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenBurn.test.ts b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenBurn.test.ts index 0e121e3b0..c6c28069a 100644 --- a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenBurn.test.ts +++ b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenBurn.test.ts @@ -6,6 +6,7 @@ import { MutationsMixin } from '@common/libs/ledger/mixin'; import { URITokenBurn, URITokenBurnInfo } from '../URITokenBurn'; import uriTokenBurnTemplate from './fixtures/URITokenBurnTx.json'; +import { AssetTypes } from '../../../factory/types'; jest.mock('@services/NetworkService'); @@ -57,6 +58,18 @@ describe('URITokenBurn tx', () => { }); }); }); + + describe('getAssetDetails()', () => { + it('should return the expected asset details', () => { + expect(info.getAssetDetails()).toStrictEqual([ + { + owner: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn', + type: AssetTypes.URIToken, + uriTokenId: 'C84F707D006E99BEA1BC0A05C9123C8FFE3B40C45625C20DA24059DE09C09C9F', + }, + ]); + }); + }); }); describe('Validation', () => {}); diff --git a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenBuy.test.ts b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenBuy.test.ts index c89a96716..275cbdc64 100644 --- a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenBuy.test.ts +++ b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenBuy.test.ts @@ -3,6 +3,7 @@ import Localize from '@locale'; import { MutationsMixin } from '@common/libs/ledger/mixin'; +import { AssetTypes } from '@common/libs/ledger/factory/types'; import { URITokenBuy, URITokenBuyInfo } from '../URITokenBuy'; import uriTokenBuy from './fixtures/URITokenBuyTx.json'; @@ -69,6 +70,18 @@ describe('URITokenBuy tx', () => { }); }); }); + + describe('getAssetDetails()', () => { + it('should return the expected asset details', () => { + expect(info.getAssetDetails()).toStrictEqual([ + { + owner: 'rrrrrrrrrrrrrrrrrrrrrholvtp', + type: AssetTypes.URIToken, + uriTokenId: '716E5990589AA8FA4247E0FEABE8B605CFFBBA5CA519A70BCA37C8CC173F3244', + }, + ]); + }); + }); }); describe('Validation', () => {}); diff --git a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenCancelSellOffer.test.ts b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenCancelSellOffer.test.ts index abe3de0c3..44a462721 100644 --- a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenCancelSellOffer.test.ts +++ b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenCancelSellOffer.test.ts @@ -3,6 +3,7 @@ import Localize from '@locale'; import { MutationsMixin } from '@common/libs/ledger/mixin'; +import { AssetTypes } from '@common/libs/ledger/factory/types'; import { URITokenCancelSellOffer, URITokenCancelSellOfferInfo } from '../URITokenCancelSellOffer'; @@ -58,6 +59,18 @@ describe('URITokenCancelSellOffer tx', () => { }); }); }); + + describe('getAssetDetails()', () => { + it('should return the expected asset details', () => { + expect(info.getAssetDetails()).toStrictEqual([ + { + owner: 'rrrrrrrrrrrrrrrrrrrrrholvtp', + type: AssetTypes.URIToken, + uriTokenId: '9CE208D4743A11AB5BAE47E23E917D456EB722A89568EDCCCA94B3B04ADC95D2', + }, + ]); + }); + }); }); describe('Validation', () => {}); diff --git a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenCreateSellOffer.test.ts b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenCreateSellOffer.test.ts index 23e8e966f..7c714c62a 100644 --- a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenCreateSellOffer.test.ts +++ b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenCreateSellOffer.test.ts @@ -7,6 +7,7 @@ import { MutationsMixin } from '@common/libs/ledger/mixin'; import { URITokenCreateSellOffer, URITokenCreateSellOfferInfo } from '../URITokenCreateSellOffer'; import uriTokenCreateSellOfferTemplate from './fixtures/URITokenCreateSellOfferTx.json'; +import { AssetTypes } from '../../../factory/types'; jest.mock('@services/NetworkService'); @@ -23,7 +24,7 @@ describe('URITokenCreateSellOffer tx', () => { const { tx, meta }: any = uriTokenCreateSellOfferTemplate; const Mixed = MutationsMixin(URITokenCreateSellOffer); const instance = new Mixed(tx, meta); - const info = new URITokenCreateSellOfferInfo(instance, {} as any); + const info = new URITokenCreateSellOfferInfo(instance, { address: tx.Account } as any); describe('generateDescription()', () => { it('should return the expected description', () => { @@ -42,6 +43,7 @@ describe('URITokenCreateSellOffer tx', () => { it('should return the expected participants', () => { expect(info.getParticipants()).toStrictEqual({ start: { address: 'rrrrrrrrrrrrrrrrrrrrrholvtp', tag: undefined }, + end: { address: 'rDestinationxxxxxxxxxxxxxxxxxxxxxx', tag: undefined }, }); }); }); @@ -51,6 +53,7 @@ describe('URITokenCreateSellOffer tx', () => { expect(info.getMonetaryDetails()).toStrictEqual({ factor: [ { + action: 'INC', currency: 'XRP', effect: 'POTENTIAL_EFFECT', value: '10', @@ -63,6 +66,18 @@ describe('URITokenCreateSellOffer tx', () => { }); }); }); + + describe('getAssetDetails()', () => { + it('should return the expected asset details', () => { + expect(info.getAssetDetails()).toStrictEqual([ + { + owner: 'rrrrrrrrrrrrrrrrrrrrrholvtp', + type: AssetTypes.URIToken, + uriTokenId: '1016FBAE4CAFB51A7E768724151964FF572495934C2D4A98CCC67229749C3F72', + }, + ]); + }); + }); }); describe('Validation', () => {}); diff --git a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenMint.test.ts b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenMint.test.ts index e6af4d716..01cd2958a 100644 --- a/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenMint.test.ts +++ b/src/common/libs/ledger/transactions/genuine/__tests__/uriTokenMint.test.ts @@ -3,6 +3,7 @@ import Localize from '@locale'; import { MutationsMixin } from '@common/libs/ledger/mixin'; +import { AssetTypes } from '@common/libs/ledger/factory/types'; import { URITokenMint, URITokenMintInfo } from '../URITokenMint'; @@ -17,6 +18,20 @@ describe('URITokenMint tx', () => { expect(instance.TransactionType).toBe('URITokenMint'); expect(instance.Type).toBe('URITokenMint'); }); + + it('Should return right parsed values', () => { + const { tx, meta }: any = uriTokenMintTemplate; + const instance = new URITokenMint(tx, meta); + + expect(instance.URITokenID).toBe('C84F707D006E99BEA1BC0A05C9123C8FFE3B40C45625C20DA24059DE09C09C9F'); + expect(instance.URI).toBe('697066733A2F2F434944'); + expect(instance.Digest).toBe('697066733A2F2F434944697066733A2F2F434944697066733A2F2F434944'); + expect(instance.Destination).toBe('rDestinationxxxxxxxxxxxxxxxxxxxxxx'); + expect(instance.Amount).toStrictEqual({ + value: '1', + currency: 'XRP', + }); + }); }); describe('Info', () => { @@ -27,7 +42,7 @@ describe('URITokenMint tx', () => { describe('generateDescription()', () => { it('should return the expected description', () => { - const expectedDescription = `The URI for this token is 697066733A2F2F434944${'\n'}The token has a digest: 697066733A2F2F434944697066733A2F2F434944697066733A2F2F434944${'\n'}The minter of this token has set the initial selling price to 1 XRP.${'\n'}This token can only be purchased by rDestinationxxxxxxxxxxxxxxxxxxxxxx`; + const expectedDescription = `The minter of this token has set the initial selling price to 1 XRP.${'\n'}This token can only be purchased by rDestinationxxxxxxxxxxxxxxxxxxxxxx${'\n'}The token has a digest: 697066733A2F2F434944697066733A2F2F434944697066733A2F2F434944${'\n'}The URI for this token is 697066733A2F2F434944`; expect(info.generateDescription()).toEqual(expectedDescription); }); }); @@ -42,6 +57,7 @@ describe('URITokenMint tx', () => { it('should return the expected participants', () => { expect(info.getParticipants()).toStrictEqual({ start: { address: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn', tag: undefined }, + end: { address: 'rDestinationxxxxxxxxxxxxxxxxxxxxxx', tag: undefined }, }); }); }); @@ -53,10 +69,29 @@ describe('URITokenMint tx', () => { DEC: [], INC: [], }, - factor: undefined, + factor: [ + { + action: 'DEC', + currency: 'XRP', + effect: 'POTENTIAL_EFFECT', + value: '1', + }, + ], }); }); }); + + describe('getAssetDetails()', () => { + it('should return the expected asset details', () => { + expect(info.getAssetDetails()).toStrictEqual([ + { + owner: 'rG1QQv2nh2gr7RCZ1P8YYcBUKCCN633jCn', + type: AssetTypes.URIToken, + uriTokenId: 'C84F707D006E99BEA1BC0A05C9123C8FFE3B40C45625C20DA24059DE09C09C9F', + }, + ]); + }); + }); }); describe('Validation', () => {}); diff --git a/src/common/libs/ledger/transactions/genuine/index.ts b/src/common/libs/ledger/transactions/genuine/index.ts index 24000d9d7..e30b8cc05 100644 --- a/src/common/libs/ledger/transactions/genuine/index.ts +++ b/src/common/libs/ledger/transactions/genuine/index.ts @@ -44,3 +44,5 @@ export * from './Remit'; export * from './Clawback'; export * from './DIDSet'; export * from './DIDDelete'; +export * from './OracleSet'; +export * from './OracleDelete'; diff --git a/src/common/libs/ledger/types/common/index.ts b/src/common/libs/ledger/types/common/index.ts index 0580fc422..992989ece 100644 --- a/src/common/libs/ledger/types/common/index.ts +++ b/src/common/libs/ledger/types/common/index.ts @@ -83,6 +83,33 @@ export interface SignerEntry { WalletLocator?: string; } +export interface PriceData { + /** + * The primary asset in a trading pair. Any valid identifier, such as a stock symbol, + * bond CUSIP, or currency code is allowed. For example, in the BTC/USD pair, BTC is the base asset; + * in 912810RR9/BTC, 912810RR9 is the base asset. + */ + BaseAsset: string; + + /** + * The quote asset in a trading pair. The quote asset denotes the price of one unit of the base asset. + * For example, in the BTC/USD pair, USD is the quote asset; in 912810RR9/BTC, BTC is the quote asset. + */ + QuoteAsset: string; + + /** + * The asset price after applying the Scale precision level. If it is not included, + * the PriceData object will be deleted. + */ + AssetPrice?: number; + + /** + * The scaling factor to apply to an asset price. For example, if Scale is 6 and original price + * is 0.155, then the scaled price is 155000. Valid scale ranges are 0-10. The default value is 0. + */ + Scale?: number; +} + /** * This information is added to Transactions in request responses, but is not part * of the canonical Transaction information on ledger. These fields are denoted with diff --git a/src/common/libs/ledger/types/enums.ts b/src/common/libs/ledger/types/enums.ts index bc43e9bd1..42bebdac5 100644 --- a/src/common/libs/ledger/types/enums.ts +++ b/src/common/libs/ledger/types/enums.ts @@ -52,6 +52,8 @@ export enum TransactionTypes { Clawback = 'Clawback', DIDDelete = 'DIDDelete', DIDSet = 'DIDSet', + OracleSet = 'OracleSet', + OracleDelete = 'OracleDelete', } /** @@ -95,6 +97,7 @@ export enum LedgerEntryTypes { RippleState = 'RippleState', SignerList = 'SignerList', EmittedTxn = 'EmittedTxn', + Oracle = 'Oracle', } /** diff --git a/src/common/libs/ledger/types/ledger/Oracle.ts b/src/common/libs/ledger/types/ledger/Oracle.ts new file mode 100644 index 000000000..0cbadf8eb --- /dev/null +++ b/src/common/libs/ledger/types/ledger/Oracle.ts @@ -0,0 +1,49 @@ +import { PriceData } from '@common/libs/ledger/types/common'; + +import { BaseLedgerEntry, HasPreviousTxnID } from './BaseLedgerEntry'; + +/* + * An Oracle ledger entry holds data associated with a single price oracle object. + */ +export default interface Oracle extends BaseLedgerEntry, HasPreviousTxnID { + /** + * The XRPL account with update and delete privileges for the oracle. + * It's recommended to set up multi-signing on this account. + */ + Owner: string; + + /** + * An arbitrary value that identifies an oracle provider, such as Chainlink, Band, or DIA. + * This field is a string, up to 256 ASCII hex encoded characters (0x20-0x7E). + */ + Provider: string; + + /** + * An array of up to 10 PriceData objects, each representing the price information for a token pair. + * More than five PriceData objects require two owner reserves. + */ + PriceDataSeries: Array; + + /** + * The time the data was last updated, represented in Unix time. + */ + LastUpdateTime: number; + + /** + * An optional Universal Resource Identifier to reference price data off-chain. + * This field is limited to 256 bytes. + */ + URI?: string; + + /** + * Describes the type of asset, such as "currency", "commodity", or "index". + * This field is a string, up to 16 ASCII hex encoded characters (0x20-0x7E). + */ + AssetClass: string; + + /** + * A hint indicating which page of the oracle owner's owner directory links to this entry, + * in case the directory consists of multiple pages. + */ + OwnerNode: string; +} diff --git a/src/common/libs/ledger/types/ledger/URIToken.ts b/src/common/libs/ledger/types/ledger/URIToken.ts index a278c6e5c..b80fae8a1 100644 --- a/src/common/libs/ledger/types/ledger/URIToken.ts +++ b/src/common/libs/ledger/types/ledger/URIToken.ts @@ -7,4 +7,6 @@ export default interface URIToken extends BaseLedgerEntry, HasPreviousTxnID { Flags: number; Owner: string; OwnerNode?: string; + // custom added by me, not really in the spec + LedgerCloseTime?: number; } diff --git a/src/common/libs/payload/object.ts b/src/common/libs/payload/object.ts index af33f181f..fa140875c 100644 --- a/src/common/libs/payload/object.ts +++ b/src/common/libs/payload/object.ts @@ -7,8 +7,6 @@ import { Endpoints } from '@common/constants/endpoints'; import ApiService, { ApiError } from '@services/ApiService'; import LoggerService from '@services/LoggerService'; -import CoreRepository from '@store/repositories/core'; - import { TransactionFactory } from '@common/libs/ledger/factory'; import Localize from '@locale'; @@ -67,8 +65,6 @@ export class Payload { } else if (isObject(args)) { // if not, assign it to the class payload.assign(args); - } else { - throw new Error('invalid args applied, only string or object'); } return payload; @@ -237,7 +233,7 @@ export class Payload { ApiService.fetch(Endpoints.Payload, 'PATCH', { uuid: this.getPayloadUUID() }, patch).catch( (error: ApiError) => { - logger.error(`Patch ${this.getPayloadUUID()}`, error); + logger.error(`patch ${this.getPayloadUUID()}`, error); }, ); @@ -330,20 +326,13 @@ export class Payload { } // check if normal transaction and supported by the app - // NOTE: only in case of developer mode enabled we allow transaction fallback if ( request_json.TransactionType && !Object.values(TransactionTypes).includes(request_json.TransactionType as TransactionTypes) ) { - if (CoreRepository.isDeveloperModeEnabled()) { - logger.warn( - `Requested transaction type "${request_json.TransactionType}" not found, revert to fallback transaction.`, - ); - } else { - throw new Error( - `Requested transaction type "${request_json.TransactionType} is not supported at the moment.`, - ); - } + logger.warn( + `Requested transaction type "${request_json.TransactionType}" not found, revert to fallback transaction.`, + ); } let craftedTransaction; diff --git a/src/common/libs/vault.ts b/src/common/libs/vault.ts index 7d6081239..fc11d50b7 100644 --- a/src/common/libs/vault.ts +++ b/src/common/libs/vault.ts @@ -32,8 +32,8 @@ const Vault = { return new Promise((resolve, reject) => { VaultManagerModule.createVault(name, entry, key) .then(resolve) - .catch((error: any) => { - logger.error('Vault create error', error); + .catch((error) => { + logger.error(`create [${name}]`, error); reject(error); }); }); @@ -53,8 +53,8 @@ const Vault = { } resolve(clearText); }) - .catch((error: Error) => { - logger.error('Vault open error', error); + .catch((error) => { + logger.error(`open [${name}]`, error); resolve(undefined); }); }); @@ -67,8 +67,8 @@ const Vault = { return new Promise((resolve, reject) => { VaultManagerModule.vaultExist(name) .then(resolve) - .catch((error: any) => { - logger.error('Vault vaultExist error', error); + .catch((error) => { + logger.error(`exist [${name}]`, error); reject(error); }); }); @@ -81,8 +81,8 @@ const Vault = { return new Promise((resolve, reject) => { VaultManagerModule.isStorageEncryptionKeyExist() .then(resolve) - .catch((error: any) => { - logger.error('Vault isStorageEncryptionKeyExist error', error); + .catch((error) => { + logger.error('isStorageEncryptionKeyExist', error); reject(error); }); }); @@ -111,8 +111,8 @@ const Vault = { resolve(keyBytes); }) - .catch((error: any) => { - logger.error('Vault getStorageEncryptionKey error', error); + .catch((error) => { + logger.error('getStorageEncryptionKey', error); reject(error); }); }); @@ -132,8 +132,8 @@ const Vault = { return new Promise((resolve, reject) => { VaultManagerModule.isMigrationRequired(name) .then(resolve) - .catch((error: any) => { - logger.error('Vault isMigrationRequired error', error); + .catch((error) => { + logger.error(`isMigrationRequired [${name}]`, error); reject(error); }); }); @@ -146,8 +146,8 @@ const Vault = { return new Promise((resolve, reject) => { VaultManagerModule.reKeyVault(name, oldKey, newKey) .then(resolve) - .catch((error: any) => { - logger.error('Vault reKey error', error); + .catch((error) => { + logger.error(`reKey [${name}]`, error); reject(error); }); }); @@ -160,8 +160,8 @@ const Vault = { return new Promise((resolve, reject) => { VaultManagerModule.reKeyBatchVaults(names, oldKey, newKey) .then(resolve) - .catch((error: any) => { - logger.error('Vault batch reKey error', error); + .catch((error) => { + logger.error('reKeyBatch', error); reject(error); }); }); @@ -172,8 +172,8 @@ const Vault = { return new Promise((resolve, reject) => { VaultManagerModule.purgeVault(name) .then(resolve) - .catch((error: any) => { - logger.error('Vault purge error', error); + .catch((error) => { + logger.error(`purge [${name}]`, error); reject(error); }); }); @@ -184,8 +184,8 @@ const Vault = { return new Promise((resolve, reject) => { VaultManagerModule.clearStorage() .then(resolve) - .catch((error: any) => { - logger.error('Vault clear storage error', error); + .catch((error) => { + logger.error('clearStorage', error); reject(error); }); }); diff --git a/src/common/utils/cache.ts b/src/common/utils/cache.ts new file mode 100644 index 000000000..37c89209e --- /dev/null +++ b/src/common/utils/cache.ts @@ -0,0 +1,38 @@ +/** + * A class that implements a Least Recently Used (LRU) cache. + * The cache has a fixed maximum size, and when this size is reached, + * the least recently used items are removed first. + * + * @template K - The type of keys maintained by this cache. + * @template V - The type of mapped values. + */ +class LRUCache { + private maxSize: number; + private cache: Map; + + constructor(maxSize: number) { + this.maxSize = maxSize; + this.cache = new Map(); + } + + get(key: K): V | undefined { + if (!this.cache.has(key)) { + return undefined; + } + + const value = this.cache.get(key)!; + this.cache.delete(key); + this.cache.set(key, value); + return value; + } + + set(key: K, value: V): void { + if (this.cache.size >= this.maxSize) { + const firstKey = this.cache.keys().next().value; + this.cache.delete(firstKey); + } + this.cache.set(key, value); + } +} + +export default LRUCache; diff --git a/src/common/utils/date.ts b/src/common/utils/date.ts index 6a2f3cedd..b7c82773d 100644 --- a/src/common/utils/date.ts +++ b/src/common/utils/date.ts @@ -9,6 +9,15 @@ const FormatDate = (date: string): string => { return moment(date).format('lll'); }; +/** + * format the unix timestamp + * @param timestamp + * @returns string September 4, 1986 8:30 PM + */ +const FormatTimestamp = (timestamp: number): string => { + return moment.unix(timestamp).format('lll'); +}; + /** * format the date * @returns string 22:30:00 @@ -19,4 +28,4 @@ const FormatTime = (time: string): string => { }; /* Export ==================================================================== */ -export { FormatDate, FormatTime }; +export { FormatDate, FormatTime, FormatTimestamp }; diff --git a/src/common/utils/string.ts b/src/common/utils/string.ts index e19082723..ce301e897 100644 --- a/src/common/utils/string.ts +++ b/src/common/utils/string.ts @@ -133,6 +133,11 @@ const StringTypeCheck = { }, isValidDestinationTag: (input: string): boolean => { + // not a valid input + if (typeof input !== 'string') { + return false; + } + // not a valid number if (!input.match(/^[+-]?\d+(?:[.]*\d*(?:[eE][+-]?\d+)?)?$/)) { return false; diff --git a/src/components/General/AccordionPicker/AccordionPicker.tsx b/src/components/General/AccordionPicker/AccordionPicker.tsx index f5e5d7bcc..476e8ff5a 100644 --- a/src/components/General/AccordionPicker/AccordionPicker.tsx +++ b/src/components/General/AccordionPicker/AccordionPicker.tsx @@ -14,7 +14,7 @@ interface Props { items: any; containerStyle?: ViewStyle; selectedItem?: any; - renderItem: (item: any, selected?: boolean) => React.ReactElement | null; + renderItem: (item: any, selected: boolean) => React.ReactElement | null; onSelect?: (item: any) => void; onExpand?: () => void; onPress?: () => void; @@ -203,7 +203,7 @@ class AccordionPicker extends Component { style={[styles.pickerDropDownItem, AppStyles.centerContent]} > - {renderItem(selectedItem)} + {renderItem(selectedItem, true)} {items.length > 1 && ( { let panelHeight = height; if (extraBottomInset) { - panelHeight += AppSizes.safeAreaBottomInset * 0.5; + panelHeight += AppSizes.safeAreaBottomInset; } const snapPoints = [{ y: screenHeight }, { y: screenHeight - panelHeight }]; diff --git a/src/components/General/Avatar/Avatar.tsx b/src/components/General/Avatar/Avatar.tsx index 50511d180..753257a9f 100644 --- a/src/components/General/Avatar/Avatar.tsx +++ b/src/components/General/Avatar/Avatar.tsx @@ -6,7 +6,7 @@ */ import React, { PureComponent } from 'react'; -import { Animated, View, Image, ImageSourcePropType, ViewStyle, InteractionManager } from 'react-native'; +import { Animated, View, Image, ImageStyle, ImageSourcePropType, ViewStyle, InteractionManager } from 'react-native'; import { Images } from '@common/helpers/images'; @@ -25,6 +25,7 @@ export interface Props { badge?: (() => React.ReactNode) | Extract; badgeColor?: string; containerStyle?: ViewStyle | ViewStyle[]; + imageStyle?: ImageStyle | ImageStyle[]; backgroundColor?: string; } @@ -138,7 +139,7 @@ class Avatar extends PureComponent { }; renderAvatar = () => { - const { source, size, imageScale, border, containerStyle } = this.props; + const { source, size, imageScale, border, imageStyle, containerStyle } = this.props; return ( { styles.image, border && styles.border, { height: AppSizes.scale(size) * imageScale, width: AppSizes.scale(size) * imageScale }, + imageStyle, ]} /> diff --git a/src/components/General/Button/Button.tsx b/src/components/General/Button/Button.tsx index 552e55a37..05242332c 100644 --- a/src/components/General/Button/Button.tsx +++ b/src/components/General/Button/Button.tsx @@ -18,6 +18,7 @@ interface Props extends PropsWithChildren { textStyle?: StyleProp; disabledStyle?: StyleProp; iconStyle?: StyleProp; + transparent?: boolean; secondary?: boolean; light?: boolean; contrast?: boolean; @@ -171,6 +172,7 @@ export default class Button extends Component { const { isDisabled, style, + transparent, secondary, light, contrast, @@ -191,8 +193,10 @@ export default class Button extends Component { testID={testID} style={[ styles.button, + transparent && styles.buttonTransparent, secondary && styles.buttonSecondary, light && styles.buttonLight, + contrast && styles.buttonContrast, rounded && styles.buttonRounded, roundedSmall && styles.buttonRoundedSmall, roundedSmallBlock && styles.buttonRoundedSmallBlock, @@ -220,6 +224,7 @@ export default class Button extends Component { delayPressIn={0} style={[ styles.button, + transparent && styles.buttonTransparent, secondary && styles.buttonSecondary, light && styles.buttonLight, contrast && styles.buttonContrast, diff --git a/src/components/General/Button/styles.ts b/src/components/General/Button/styles.ts index 066a8fd15..02550e24e 100644 --- a/src/components/General/Button/styles.ts +++ b/src/components/General/Button/styles.ts @@ -36,6 +36,10 @@ const styles = StyleService.create({ alignSelf: 'stretch', }, + buttonTransparent: { + backgroundColor: '$transparent', + }, + // Secondary buttonSecondary: { backgroundColor: '$grey', diff --git a/src/components/General/CardFlip/CardFlip.tsx b/src/components/General/CardFlip/CardFlip.tsx deleted file mode 100644 index 8fa70102f..000000000 --- a/src/components/General/CardFlip/CardFlip.tsx +++ /dev/null @@ -1,357 +0,0 @@ -// https://github.com/lhandel/react-native-card-flip - -import React, { Component } from 'react'; - -import { Platform, Animated, StyleProp, ViewStyle } from 'react-native'; - -import styles from './styles'; -/* Types ==================================================================== */ -export type FlipDirection = 'y' | 'x'; - -export type Direction = 'right' | 'left'; - -interface Props { - style?: StyleProp; - duration?: number; - flipZoom?: number; - flipDirection?: FlipDirection; - onFlip?: (index: number) => void; - onFlipEnd?: (index: number) => void; - onFlipStart?: (index: number) => void; - perspective?: number; - children: JSX.Element[]; -} - -interface State { - duration: number; - flipZoom?: number; - flipDirection?: FlipDirection; - side: number; - sides: JSX.Element[]; - progress: Animated.Value; - rotation: Animated.ValueXY; - zoom: Animated.Value; - rotateOrientation: FlipDirection; -} - -/* Component ==================================================================== */ -class CardFlip extends Component { - declare readonly props: Props & Required>; - - static defaultProps: Partial = { - style: {}, - duration: 500, - flipZoom: 0.09, - flipDirection: 'y', - perspective: 800, - onFlip: () => {}, - onFlipStart: () => {}, - onFlipEnd: () => {}, - }; - - constructor(props: Props) { - super(props); - this.state = { - duration: 5000, - side: 0, - sides: [], - progress: new Animated.Value(0), - rotation: new Animated.ValueXY({ x: 50, y: 50 }), - zoom: new Animated.Value(0), - rotateOrientation: 'y', - }; - } - - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - if ( - nextProps.duration !== prevState.duration || - nextProps.flipZoom !== prevState.flipZoom || - nextProps.children !== prevState.sides - ) { - return { - duration: nextProps.duration, - flipZoom: nextProps.flipZoom, - sides: nextProps.children, - }; - } - return null; - } - - componentDidMount() { - const { duration, flipZoom, children } = this.props; - this.setState({ - duration, - flipZoom, - sides: children, - }); - } - - tip(customConfig: any) { - const defaultConfig = { direction: 'left', progress: 0.05, duration: 150 }; - const config = { ...defaultConfig, ...customConfig }; - const { direction, progress, duration } = config; - - const { rotation, side } = this.state; - const sequence = []; - - if (direction === 'right') { - sequence.push( - Animated.timing(rotation, { - toValue: { - x: 0, - y: side === 0 ? 50 + progress * 50 : 90, - }, - duration, - useNativeDriver: true, - }), - ); - } else { - sequence.push( - Animated.timing(rotation, { - toValue: { - x: 0, - y: side === 0 ? 50 - progress * 50 : 90, - }, - duration, - useNativeDriver: true, - }), - ); - } - sequence.push( - Animated.timing(rotation, { - toValue: { - x: 0, - y: side === 0 ? 50 : 100, - }, - duration, - useNativeDriver: true, - }), - ); - Animated.sequence(sequence).start(); - } - - jiggle(customConfig = {}) { - const defaultConfig = { count: 2, duration: 100, progress: 0.05 }; - const config = { ...defaultConfig, ...customConfig }; - - const { count, duration, progress } = config; - - const { rotation, side } = this.state; - - const sequence = []; - for (let i = 0; i < count; i++) { - sequence.push( - Animated.timing(rotation, { - toValue: { - x: 0, - y: side === 0 ? 50 + progress * 50 : 90, - }, - duration, - useNativeDriver: true, - }), - ); - - sequence.push( - Animated.timing(rotation, { - toValue: { - x: 0, - y: side === 0 ? 50 - progress * 50 : 110, - }, - duration, - useNativeDriver: true, - }), - ); - } - sequence.push( - Animated.timing(rotation, { - toValue: { - x: 0, - y: side === 0 ? 50 : 100, - }, - duration, - useNativeDriver: true, - }), - ); - Animated.sequence(sequence).start(); - } - - flip() { - const { flipDirection } = this.props; - if (flipDirection === 'y') { - this.flipY(); - } else { - this.flipX(); - } - } - - flipY() { - const { side } = this.state; - this.flipTo({ - x: 50, - y: side === 0 ? 100 : 50, - }); - this.setState({ - side: side === 0 ? 1 : 0, - rotateOrientation: 'y', - }); - } - - flipX() { - const { side } = this.state; - this.flipTo({ - y: 50, - x: side === 0 ? 100 : 50, - }); - this.setState({ - side: side === 0 ? 1 : 0, - rotateOrientation: 'x', - }); - } - - private flipTo(toValue: any) { - const { onFlip, onFlipStart, onFlipEnd } = this.props; - const { duration, rotation, progress, zoom, side } = this.state; - - onFlip(side === 0 ? 1 : 0); - onFlipStart(side === 0 ? 1 : 0); - - Animated.parallel([ - Animated.timing(progress, { - toValue: side === 0 ? 100 : 0, - duration, - useNativeDriver: true, - }), - Animated.sequence([ - Animated.timing(zoom, { - toValue: 100, - duration: duration / 2, - useNativeDriver: true, - }), - Animated.timing(zoom, { - toValue: 0, - duration: duration / 2, - useNativeDriver: true, - }), - ]), - Animated.timing(rotation, { - toValue, - duration, - useNativeDriver: true, - }), - ]).start(() => { - onFlipEnd(side === 0 ? 1 : 0); - }); - } - - getCardATransformation() { - const { perspective } = this.props; - const { progress, rotation, side, rotateOrientation } = this.state; - - const sideAOpacity = progress.interpolate({ - inputRange: [50, 51], - outputRange: [100, 0], - extrapolate: 'clamp', - }); - - const sideATransform = { - opacity: sideAOpacity, - zIndex: side === 0 ? 1 : 0, - transform: [{ perspective }], - }; - if (rotateOrientation === 'x') { - const aXRotation = rotation.x.interpolate({ - inputRange: [0, 50, 100, 150], - outputRange: ['-180deg', '0deg', '180deg', '0deg'], - extrapolate: 'clamp', - }); - // @ts-ignore - sideATransform.transform.push({ rotateX: aXRotation }); - } else { - // cardA Y-rotation - const aYRotation = rotation.y.interpolate({ - inputRange: [0, 50, 100, 150], - outputRange: ['-180deg', '0deg', '180deg', '0deg'], - extrapolate: 'clamp', - }); - // @ts-ignore - sideATransform.transform.push({ rotateY: aYRotation }); - } - return sideATransform; - } - - getCardBTransformation() { - const { progress, rotation, side, rotateOrientation } = this.state; - const { perspective } = this.props; - - let bYRotation; - - const sideBOpacity = progress.interpolate({ - inputRange: [50, 51], - outputRange: [0, 100], - extrapolate: 'clamp', - }); - - const sideBTransform = { - opacity: sideBOpacity, - zIndex: side === 0 ? 0 : 1, - transform: [{ perspective: -1 * perspective }], - }; - - if (rotateOrientation === 'x') { - const bXRotation = rotation.x.interpolate({ - inputRange: [0, 50, 100, 150], - outputRange: ['0deg', '-180deg', '-360deg', '180deg'], - extrapolate: 'clamp', - }); - // @ts-ignore - sideBTransform.transform.push({ rotateX: bXRotation }); - } else { - if (Platform.OS === 'ios') { - // cardB Y-rotation - bYRotation = rotation.y.interpolate({ - inputRange: [0, 50, 100, 150], - outputRange: ['0deg', '180deg', '0deg', '-180deg'], - extrapolate: 'clamp', - }); - } else { - // cardB Y-rotation - bYRotation = rotation.y.interpolate({ - inputRange: [0, 50, 100, 150], - outputRange: ['0deg', '-180deg', '0deg', '180deg'], - extrapolate: 'clamp', - }); - } - // @ts-ignore - sideBTransform.transform.push({ rotateY: bYRotation }); - } - return sideBTransform; - } - - render() { - const { zoom, sides } = this.state; - const { flipZoom, style } = this.props; - - const cardATransform = this.getCardATransformation(); - - const cardBTransform = this.getCardBTransformation(); - - const cardZoom = zoom.interpolate({ - inputRange: [0, 100], - outputRange: [1, 1 + flipZoom], - extrapolate: 'clamp', - }); - - const scaling = { - transform: [{ scale: cardZoom }], - }; - - return ( - - {sides[0]} - {sides[1]} - - ); - } -} - -export default CardFlip; diff --git a/src/components/General/CardFlip/index.ts b/src/components/General/CardFlip/index.ts deleted file mode 100644 index b383a9464..000000000 --- a/src/components/General/CardFlip/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CardFlip } from './CardFlip'; diff --git a/src/components/General/CardFlip/styles.ts b/src/components/General/CardFlip/styles.ts deleted file mode 100644 index 7fe7b70c2..000000000 --- a/src/components/General/CardFlip/styles.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { StyleSheet } from 'react-native'; - -const styles = StyleSheet.create({ - cardContainer: { - flex: 1, - position: 'absolute', - left: 0, - right: 0, - bottom: 0, - top: 0, - }, -}); - -export default styles; diff --git a/src/components/General/CountDown/CountDown.tsx b/src/components/General/CountDown/CountDown.tsx new file mode 100644 index 000000000..54a3863ec --- /dev/null +++ b/src/components/General/CountDown/CountDown.tsx @@ -0,0 +1,93 @@ +/** + * CountDown component + * + + * + */ +import React, { PureComponent } from 'react'; + +import { Animated, InteractionManager, Text, TextStyle } from 'react-native'; + +/* Types ==================================================================== */ +export interface Props { + seconds: number; + preFix?: string; + postFix?: string; + style: TextStyle | TextStyle[]; + onFinish?: () => void; +} + +interface State { + current: number; +} + +/* Component ==================================================================== */ +class CountDown extends PureComponent { + private _isMounted: boolean; + + private countDownAnimated: Animated.Value; + + constructor(props: Props) { + super(props); + + this.state = { + current: props.seconds, + }; + + this._isMounted = false; + this.countDownAnimated = new Animated.Value(props.seconds); + this.countDownAnimated.addListener(this.onValueChange); + } + componentDidMount() { + this._isMounted = true; + InteractionManager.runAfterInteractions(this.startCountDown); + } + + componentWillUnmount() { + this._isMounted = false; + this.countDownAnimated.removeAllListeners(); + } + + onValueChange = ({ value }: { value: number }) => { + const { current } = this.state; + + if (!this._isMounted) { + return; + } + + const fixed = Math.floor(value) + 1; + + if (fixed !== current) { + this.setState({ + current: fixed, + }); + } + }; + + startCountDown = () => { + const { onFinish, seconds } = this.props; + + Animated.timing(this.countDownAnimated, { toValue: 0, duration: seconds * 1000, useNativeDriver: true }).start( + () => { + if (typeof onFinish === 'function') { + onFinish(); + } + }, + ); + }; + + render() { + const { style, preFix, postFix } = this.props; + const { current } = this.state; + + return ( + + {preFix} {current} + {postFix} + + ); + } +} + +/* Export Component ==================================================================== */ +export default CountDown; diff --git a/src/components/General/CountDown/index.ts b/src/components/General/CountDown/index.ts new file mode 100644 index 000000000..6af4a46cd --- /dev/null +++ b/src/components/General/CountDown/index.ts @@ -0,0 +1 @@ +export { default as CountDown, type Props as CountDownProps } from './CountDown'; diff --git a/src/components/General/CountDown/styles.ts b/src/components/General/CountDown/styles.ts new file mode 100644 index 000000000..a7ab268ff --- /dev/null +++ b/src/components/General/CountDown/styles.ts @@ -0,0 +1,33 @@ +import StyleService from '@services/StyleService'; + +/* Styles ==================================================================== */ +export default StyleService.create({ + container: { + borderRadius: 11, + backgroundColor: '$tint', + alignItems: 'center', + justifyContent: 'center', + }, + placeholder: { + backgroundColor: '$grey', + }, + image: { + borderRadius: 11, + }, + border: { + borderColor: '$lightGrey', + borderWidth: 1, + }, + badgeContainer: { + position: 'absolute', + }, + badgeContainerText: { + position: 'absolute', + backgroundColor: '$blue', + borderWidth: 2.5, + borderColor: '$background', + }, + badge: { + tintColor: '$white', + }, +}); diff --git a/src/components/General/Header/Header.tsx b/src/components/General/Header/Header.tsx index 5246d29d8..ee3836da0 100644 --- a/src/components/General/Header/Header.tsx +++ b/src/components/General/Header/Header.tsx @@ -16,7 +16,7 @@ type placementType = 'left' | 'right' | 'center'; interface ChildrenProps { testID?: string; text?: string; - textStyle?: TextStyle; + textStyle?: TextStyle | TextStyle[]; icon?: Extract; iconSize?: number; iconStyle?: ImageStyle; diff --git a/src/components/General/HeartBeatAnimation/HeartBeatAnimation.tsx b/src/components/General/HeartBeatAnimation/HeartBeatAnimation.tsx index 6e15d12ac..b98f4827d 100644 --- a/src/components/General/HeartBeatAnimation/HeartBeatAnimation.tsx +++ b/src/components/General/HeartBeatAnimation/HeartBeatAnimation.tsx @@ -5,16 +5,23 @@ * */ import React, { PureComponent } from 'react'; -import { View, Animated, ViewStyle } from 'react-native'; +import { View, Animated, ViewStyle, InteractionManager } from 'react-native'; /* Types ==================================================================== */ interface Props { + animated?: boolean; children: React.ReactNode; containerStyle?: ViewStyle | ViewStyle[]; } /* Component ==================================================================== */ class HeartBeatAnimation extends PureComponent { + declare readonly props: Props & Required>; + + static defaultProps: Partial = { + animated: true, + }; + private scaleAnimation: Animated.Value; constructor(props: Props) { @@ -24,10 +31,26 @@ class HeartBeatAnimation extends PureComponent { } componentDidMount() { - this.startAnimation(); + InteractionManager.runAfterInteractions(this.startAnimation); + } + + componentDidUpdate(prevProps: Props) { + const { animated } = this.props; + + if (animated !== prevProps.animated) { + if (animated) { + this.startAnimation(); + } else { + this.stopAnimation(); + } + } } startAnimation = () => { + const { animated } = this.props; + + if (!animated) return; + Animated.loop( Animated.sequence([ Animated.timing(this.scaleAnimation, { @@ -44,6 +67,10 @@ class HeartBeatAnimation extends PureComponent { ).start(); }; + stopAnimation = () => { + this.scaleAnimation.stopAnimation(); + }; + render() { const { children, containerStyle } = this.props; diff --git a/src/components/General/Icon/Icon.tsx b/src/components/General/Icon/Icon.tsx index 3e7279d5e..2f618c136 100644 --- a/src/components/General/Icon/Icon.tsx +++ b/src/components/General/Icon/Icon.tsx @@ -1,7 +1,7 @@ /** - * Footer + * Icon * -
+ * */ import React from 'react'; diff --git a/src/components/General/NativePaymentButton/NativePaymentButton.tsx b/src/components/General/NativePaymentButton/NativePaymentButton.tsx deleted file mode 100644 index ea5380c37..000000000 --- a/src/components/General/NativePaymentButton/NativePaymentButton.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { debounce } from 'lodash'; - -import React, { PureComponent } from 'react'; - -import { HostComponent, requireNativeComponent, ViewStyle, StyleProp } from 'react-native'; - -import { LoadingIndicator } from '@components/General/LoadingIndicator'; - -import { AppSizes } from '@theme'; - -import styles from './styles'; -import StyleService from '@services/StyleService'; -/* Types ==================================================================== */ -interface Props { - buttonStyle?: 'light' | 'dark'; - testID?: string; - onPress?: () => void; - isLoading?: boolean; - style?: StyleProp | undefined; -} - -/* Native ==================================================================== */ -const NativePayButton: HostComponent = requireNativeComponent('NativePayButton'); - -/* Component ==================================================================== */ -export default class NativePaymentButton extends PureComponent { - debouncedOnPress = () => { - const { onPress, isLoading } = this.props; - - if (isLoading) { - return; - } - - if (typeof onPress === 'function') { - onPress(); - } - }; - - onPress = debounce(this.debouncedOnPress, 500, { leading: true, trailing: false }); - - render() { - const { buttonStyle, testID, style, isLoading } = this.props; - return ( - <> - - - - ); - } -} diff --git a/src/components/General/NativePaymentButton/index.ts b/src/components/General/NativePaymentButton/index.ts deleted file mode 100644 index 421b9e18a..000000000 --- a/src/components/General/NativePaymentButton/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as NativePaymentButton } from './NativePaymentButton'; diff --git a/src/components/General/NativePaymentButton/styles.ts b/src/components/General/NativePaymentButton/styles.ts deleted file mode 100644 index a84948782..000000000 --- a/src/components/General/NativePaymentButton/styles.ts +++ /dev/null @@ -1,14 +0,0 @@ -import StyleService from '@services/StyleService'; - -const styles = StyleService.create({ - loadingIndicator: { - position: 'absolute', - alignSelf: 'center', - top: '14%', - }, - payButtonLoading: { - opacity: 0.5, - }, -}); - -export default styles; diff --git a/src/components/General/ReadMore/ReadMore.tsx b/src/components/General/ReadMore/ReadMore.tsx index e25de2678..677582c19 100644 --- a/src/components/General/ReadMore/ReadMore.tsx +++ b/src/components/General/ReadMore/ReadMore.tsx @@ -96,12 +96,8 @@ class ReadMore extends Component { renderViewMore = () => { return ( - - {Localize.t('global.readMore')} + + {Localize.t('global.readMore')} ); @@ -109,12 +105,8 @@ class ReadMore extends Component { renderViewLess = () => { return ( - - {Localize.t('global.readLess')} + + {Localize.t('global.readLess')} ); diff --git a/src/components/General/ReadMore/styles.ts b/src/components/General/ReadMore/styles.ts index 5fd0f28d5..cb4e80db6 100644 --- a/src/components/General/ReadMore/styles.ts +++ b/src/components/General/ReadMore/styles.ts @@ -1,6 +1,6 @@ import StyleService from '@services/StyleService'; -import { AppFonts } from '@theme'; +import { AppFonts, AppSizes } from '@theme'; /* Styles ==================================================================== */ export default StyleService.create({ @@ -10,9 +10,14 @@ export default StyleService.create({ left: 0, top: 0, }, - viewMoreText: { + actionButton: { + flexDirection: 'row', + paddingTop: AppSizes.paddingExtraSml, + alignSelf: 'center', + }, + actionButtonText: { fontFamily: AppFonts.base.familyBold, - fontSize: AppFonts.subtext.size, + fontSize: AppFonts.small.size, marginRight: 5, color: '$grey', }, diff --git a/src/components/General/Slider/Slider.tsx b/src/components/General/Slider/Slider.tsx index 94d499dde..e19321112 100644 --- a/src/components/General/Slider/Slider.tsx +++ b/src/components/General/Slider/Slider.tsx @@ -2,23 +2,28 @@ /* eslint-disable react/jsx-props-no-spreading */ import React, { Component, Children } from 'react'; -import { View, ScrollView, Animated, ViewStyle } from 'react-native'; +import { View, ScrollView, Animated, ViewStyle, InteractionManager } from 'react-native'; import Indicator from './PageIndicator'; import styles from './styles'; /* Constants ==================================================================== */ -const floatEpsilon = 2 ** -23; +const FLOAT_EPSILON = 2 ** -23; +const AUTO_SCROLL_INTERVAL = 5000; // 5s function equal(a: number, b: number) { - return Math.abs(a - b) <= floatEpsilon * Math.max(Math.abs(a), Math.abs(b)); + return Math.abs(a - b) <= FLOAT_EPSILON * Math.max(Math.abs(a), Math.abs(b)); } /* Types ==================================================================== */ interface Props { children: React.ReactNode; style?: ViewStyle; + scrollAutomatically?: boolean; + scrollEnabled?: boolean; + horizontal?: boolean; + showsVerticalScrollIndicator?: boolean; pagingEnabled?: boolean; showsHorizontalScrollIndicator?: boolean; scrollEventThrottle?: number; @@ -45,6 +50,7 @@ export default class Slider extends Component { private progress: number; private mounted: boolean; private scrollState: number; + private autoScrollInterval: NodeJS.Timeout | null; private scrollRef: React.RefObject; @@ -57,6 +63,7 @@ export default class Slider extends Component { scrollsToTop: false, indicatorOpacity: 0.3, startPage: 0, + scrollAutomatically: false, }; constructor(props: Props) { @@ -71,18 +78,48 @@ export default class Slider extends Component { this.progress = props.startPage; this.mounted = false; this.scrollState = -1; + this.autoScrollInterval = null; this.scrollRef = React.createRef(); } componentDidMount() { this.mounted = true; + + InteractionManager.runAfterInteractions(this.startAutoScroll); } componentWillUnmount() { this.mounted = false; + + this.stopAutoScroll(); } + startAutoScroll = () => { + const { scrollAutomatically } = this.props; + + if (scrollAutomatically) { + this.autoScrollInterval = setInterval(this.autoScroll, AUTO_SCROLL_INTERVAL); + } + }; + + stopAutoScroll = () => { + if (this.autoScrollInterval) { + clearInterval(this.autoScrollInterval); + } + }; + + autoScroll = () => { + const { children } = this.props; + const { progress } = this.state; + + const pages = Children.count(children); + // @ts-ignore __getValue + const nextPage = (Math.round(progress.__getValue()) + 1) % pages; + + this.scrollToPage(nextPage); + }; + onLayout = (event: any) => { const { width, height } = event.nativeEvent.layout; const { onLayout } = this.props; @@ -109,6 +146,7 @@ export default class Slider extends Component { onScrollBeginDrag = () => { this.scrollState = 0; + this.stopAutoScroll(); // stop auto scroll when user begins to drag }; onScrollEndDrag = () => { @@ -207,6 +245,7 @@ export default class Slider extends Component { onScrollBeginDrag={this.onScrollBeginDrag} onScrollEndDrag={this.onScrollEndDrag} ref={this.scrollRef} + horizontal > {Children.map(children, this.renderPage)} diff --git a/src/components/General/__tests__/AccordionPicker.test.tsx b/src/components/General/__tests__/AccordionPicker.test.tsx new file mode 100644 index 000000000..454ef2439 --- /dev/null +++ b/src/components/General/__tests__/AccordionPicker.test.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Text } from 'react-native'; + +import renderer from 'react-test-renderer'; + +import { AccordionPicker } from '../AccordionPicker'; + +const mockItems = [ + { id: 1, name: 'Item 1' }, + { id: 2, name: 'Item 2' }, +]; +const mockKeyExtractor = (item: any) => item.id; +const mockRenderItem = (item: any, isSelected: boolean) => ( + {isSelected ? `Selected ${item.name}` : item.name} +); + +describe('[AccordionPicker]', () => { + it('renders correctly with items', () => { + const tree = renderer + .create() + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); + + it('shows "No Item available" when items list is empty', () => { + const tree = renderer + .create() + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/General/__tests__/AmountInput.test.tsx b/src/components/General/__tests__/AmountInput.test.tsx new file mode 100644 index 000000000..c49905096 --- /dev/null +++ b/src/components/General/__tests__/AmountInput.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { TextInput } from 'react-native'; +import renderer from 'react-test-renderer'; + +import { AmountInput, AmountValueType } from '../AmountInput'; + +describe('[AmountInput]', () => { + it('renders correctly with default props', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with specific props', () => { + const tree = renderer + .create( + , + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('calls onChange when value changes', () => { + const onChangeMock = jest.fn(); + const instance = renderer.create( + , + ).root; + + const textInput = instance.findByType(TextInput); + + textInput.props.onChangeText('100'); + + expect(onChangeMock).toHaveBeenCalledWith('100'); + }); + + it('formats value correctly based on value type and fractional prop', () => { + const instance = renderer.create( + , + ).root; + + expect(instance.findByType(TextInput).props.value).toBe('123456789.1234567'); + }); +}); diff --git a/src/components/General/__tests__/Avatar.test.tsx b/src/components/General/__tests__/Avatar.test.tsx new file mode 100644 index 000000000..af916f5ef --- /dev/null +++ b/src/components/General/__tests__/Avatar.test.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { Text } from 'react-native'; + +import renderer from 'react-test-renderer'; + +import { Avatar } from '../Avatar'; + +jest.useFakeTimers(); + +describe('[Avatar]', () => { + it('should render correctly', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should render correctly with loading state', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should render correctly with a badge', () => { + const tree = renderer + .create( Badge} />) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('should apply styles correctly', () => { + const tree = renderer + .create( + , + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/General/__tests__/Badge.test.tsx b/src/components/General/__tests__/Badge.test.tsx new file mode 100644 index 000000000..c62cb322b --- /dev/null +++ b/src/components/General/__tests__/Badge.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import renderer from 'react-test-renderer'; + +import { AppColors } from '@theme'; + +import { TouchableDebounce } from '../TouchableDebounce'; +import { Badge, BadgeType } from '../Badge'; + +describe('[Badge]', () => { + it('renders correctly with default props', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with a label', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with specified type', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('applies correct color based on the type', () => { + const tree = renderer.create(); + expect(tree.toJSON()).toMatchSnapshot(); + const backgroundColorStyle = tree.root.findByType(TouchableDebounce).props.style[1].backgroundColor; + expect(backgroundColorStyle).toEqual(AppColors.brandXrplns); + }); + + it('calls onPress callback when pressed', () => { + const onPressMock = jest.fn(); + const tree = renderer.create(); + tree.root.findByType(TouchableDebounce).props.onPress(); + expect(onPressMock).toHaveBeenCalled(); + }); + + it('renders correctly with different sizes', () => { + const treeSmall = renderer.create().toJSON(); + expect(treeSmall).toMatchSnapshot(); + + const treeMedium = renderer.create().toJSON(); + expect(treeMedium).toMatchSnapshot(); + + const treeLarge = renderer.create().toJSON(); + expect(treeLarge).toMatchSnapshot(); + }); +}); diff --git a/src/components/General/__tests__/Button.test.tsx b/src/components/General/__tests__/Button.test.tsx index 2091a2627..82ef0e380 100644 --- a/src/components/General/__tests__/Button.test.tsx +++ b/src/components/General/__tests__/Button.test.tsx @@ -6,7 +6,7 @@ import { Text, ActivityIndicator } from 'react-native'; import { Button } from '..'; -describe.skip('Button Component', () => { +describe.skip('[Button]', () => { beforeEach(() => { jest.resetModules(); }); diff --git a/src/components/General/__tests__/CheckBox.test.tsx b/src/components/General/__tests__/CheckBox.test.tsx new file mode 100644 index 000000000..2ac522669 --- /dev/null +++ b/src/components/General/__tests__/CheckBox.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import renderer from 'react-test-renderer'; + +import { CheckBox } from '../CheckBox'; + +describe('[CheckBox]', () => { + it('renders correctly when checked', () => { + const tree = renderer + .create( + {}} + checked + label="Test Label" + labelSmall="Test Label Small" + description="Test Description" + testID="testCheckBox" + />, + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly when unchecked', () => { + const tree = renderer + .create( + {}} + checked={false} + label="Test Label Unchecked" + labelSmall="Test Label Small Unchecked" + description="Test Description Unchecked" + testID="testCheckBoxUnchecked" + />, + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/General/__tests__/DerivationPathInput.test.tsx b/src/components/General/__tests__/DerivationPathInput.test.tsx new file mode 100644 index 000000000..ef84620b5 --- /dev/null +++ b/src/components/General/__tests__/DerivationPathInput.test.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { TextInput } from 'react-native'; + +import renderer from 'react-test-renderer'; + +import { DerivationPathInput } from '../DerivationPathInput'; + +jest.useFakeTimers(); + +describe('[DerivationPathInput]', () => { + it('should match snapshot', () => { + const testRenderer = renderer.create(); + expect(testRenderer.toJSON()).toMatchSnapshot(); + }); + + it('should call onChange with correct values', () => { + const mockOnChange = jest.fn(); + const testRenderer = renderer.create(); + + const instance = testRenderer.root; + + const accountPathInput = instance.findAllByType(TextInput)[0]; + const changePathInput = instance.findAllByType(TextInput)[1]; + const addressIndexInput = instance.findAllByType(TextInput)[2]; + + accountPathInput.props.onChangeText('1'); + changePathInput.props.onChangeText('23'); + addressIndexInput.props.onChangeText('456'); + + expect(mockOnChange).toHaveBeenCalledWith({ + accountPath: '1', + changePath: '23', + addressIndex: '456', + }); + }); + + it('should focus the accountPath input if autoFocus is set', () => { + const testRenderer = renderer.create(); + const instance = testRenderer.root; + const accountPathInput = instance.findAllByType(TextInput)[0]; + + // is focused? + setTimeout(() => { + expect(accountPathInput.props.focus).toBeTruthy(); + }, 300); + }); +}); diff --git a/src/components/General/__tests__/ExpandableView.test.tsx b/src/components/General/__tests__/ExpandableView.test.tsx new file mode 100644 index 000000000..3d1c29390 --- /dev/null +++ b/src/components/General/__tests__/ExpandableView.test.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import { Text } from 'react-native'; + +import renderer from 'react-test-renderer'; + +import { TouchableDebounce } from '../TouchableDebounce'; +import { ExpandableView } from '../ExpandableView'; + +jest.useFakeTimers(); + +describe('[ExpandableView]', () => { + const defaultProps = { + title: 'Expandable Title', + titleStyle: {}, + children: Expandable Content, + }; + + it('renders correctly when collapsed', () => { + // eslint-disable-next-line react/jsx-props-no-spreading + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly when expanded', () => { + // eslint-disable-next-line react/jsx-props-no-spreading + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('expands when footer is pressed', () => { + // eslint-disable-next-line react/jsx-props-no-spreading + const component = renderer.create(); + const instance = component.root; + const footerButton = instance.findByType(TouchableDebounce); + + expect(instance.instance.state.expanded).toBe(false); + + renderer.act(() => { + footerButton.props.onPress(); + }); + + expect(instance.instance.state.expanded).toBe(true); + }); + + it('collapses when footer is pressed again', () => { + // eslint-disable-next-line react/jsx-props-no-spreading + const component = renderer.create(); + const instance = component.root; + const footerButton = instance.findByType(TouchableDebounce); + + expect(instance.instance.state.expanded).toBe(true); + + renderer.act(() => { + footerButton.props.onPress(); + }); + + expect(instance.instance.state.expanded).toBe(false); + }); +}); diff --git a/src/components/General/__tests__/Header.test.tsx b/src/components/General/__tests__/Header.test.tsx new file mode 100644 index 000000000..1d087143c --- /dev/null +++ b/src/components/General/__tests__/Header.test.tsx @@ -0,0 +1,72 @@ +/** + * @format + */ + +import React from 'react'; +import renderer from 'react-test-renderer'; + +import { Header } from '../Header'; + +describe('[Header]', () => { + it('renders correctly with default props', () => { + const tree = renderer.create(
).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with leftComponent', () => { + const leftComponent = { + text: 'Left', + }; + const tree = renderer.create(
).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with centerComponent', () => { + const centerComponent = { + text: 'Center', + }; + const tree = renderer.create(
).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with rightComponent', () => { + const rightComponent = { + text: 'Right', + }; + const tree = renderer.create(
).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with all components', () => { + const leftComponent = { + text: 'Left', + }; + const centerComponent = { + text: 'Center', + }; + const rightComponent = { + text: 'Right', + }; + const tree = renderer + .create( +
, + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with backgroundColor', () => { + const tree = renderer.create(
).toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with containerStyle', () => { + // eslint-disable-next-line react-native/no-color-literals,react-native/no-inline-styles + const tree = renderer.create(
).toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/General/__tests__/HorizontalLine.test.tsx b/src/components/General/__tests__/HorizontalLine.test.tsx new file mode 100644 index 000000000..94dee822d --- /dev/null +++ b/src/components/General/__tests__/HorizontalLine.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { View } from 'react-native'; +import renderer from 'react-test-renderer'; + +import { HorizontalLine } from '../HorizontalLine'; + +import StyleService from '@services/StyleService'; + +describe('[HorizontalLine]', () => { + it('should render with default props', () => { + const component = renderer.create(); + const { root } = component; + + expect(component.toJSON()).toMatchSnapshot(); + expect(root.findByType(View).props.style).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + borderBottomColor: StyleService.value('$lightGrey'), + borderBottomWidth: 2, + width: '100%', + }), + ]), + ); + }); + + it('should render with custom props', () => { + const customStyle = { marginBottom: 10 }; + const component = renderer.create(); + const { root } = component; + + expect(component.toJSON()).toMatchSnapshot(); + expect(root.findByType(View).props.style).toEqual( + expect.arrayContaining([ + expect.objectContaining(customStyle), + expect.objectContaining({ + borderBottomColor: 'red', + borderBottomWidth: 4, + width: '50%', + }), + ]), + ); + }); +}); diff --git a/src/components/General/__tests__/Icon.test.tsx b/src/components/General/__tests__/Icon.test.tsx new file mode 100644 index 000000000..576150eac --- /dev/null +++ b/src/components/General/__tests__/Icon.test.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { Image } from 'react-native'; +import renderer from 'react-test-renderer'; + +import { Images } from '@common/helpers/images'; + +import { Icon } from '../Icon'; + +describe('[Icon]', () => { + it('renders correctly with default props', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with custom size', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders correctly with custom style', () => { + const customStyle = { tintColor: 'red' }; + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('applies the correct image source', () => { + const component = renderer.create(); + const imageInstance = component.root.findByType(Image); + expect(imageInstance.props.source).toEqual(Images.IconFlaskConical); + }); + + it('applies the correct size style', () => { + const size = 50; + const component = renderer.create(); + const imageInstance = component.root.findByType(Image); + expect(imageInstance.props.style).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + width: 78.57142857142857, + height: 78.57142857142857, + }), + ]), + ); + }); +}); diff --git a/src/components/General/__tests__/LoadingIndicator.test.tsx b/src/components/General/__tests__/LoadingIndicator.test.tsx new file mode 100644 index 000000000..7779c3aca --- /dev/null +++ b/src/components/General/__tests__/LoadingIndicator.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import StyleService from '@services/StyleService'; + +import { LoadingIndicator } from '../LoadingIndicator'; + +describe('[LoadingIndicator]', () => { + it('renders with default props', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders with specified color (light)', () => { + const spyStyleService = jest.spyOn(StyleService, 'value'); + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + expect(spyStyleService).toHaveBeenCalledWith('$white'); + }); + + it('renders with specified color (dark)', () => { + const spyStyleService = jest.spyOn(StyleService, 'value'); + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + expect(spyStyleService).toHaveBeenCalledWith('$black'); + }); + + it('renders with specified size (large)', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders while animating is false', () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it('renders with custom styles', () => { + const customStyle = { margin: 10 }; + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/General/__tests__/RadioButton.test.tsx b/src/components/General/__tests__/RadioButton.test.tsx index 79ca2545a..84676ebdd 100644 --- a/src/components/General/__tests__/RadioButton.test.tsx +++ b/src/components/General/__tests__/RadioButton.test.tsx @@ -1,9 +1,4 @@ -/** - * @format - */ - import React from 'react'; -// Note: test renderer must be required after react-native. import renderer from 'react-test-renderer'; import { TouchableOpacity } from 'react-native'; diff --git a/src/components/General/__tests__/TextAvatar.test.tsx b/src/components/General/__tests__/TextAvatar.test.tsx index 60defce4e..adc3c962f 100644 --- a/src/components/General/__tests__/TextAvatar.test.tsx +++ b/src/components/General/__tests__/TextAvatar.test.tsx @@ -8,7 +8,7 @@ import renderer from 'react-test-renderer'; import { Text } from 'react-native'; -import { TextAvatar } from '..'; +import { TextAvatar } from '../TextAvatar'; describe('[TextAvatar]', () => { it('renders correctly', () => { diff --git a/src/components/General/__tests__/__snapshots__/AccordionPicker.test.tsx.snap b/src/components/General/__tests__/__snapshots__/AccordionPicker.test.tsx.snap new file mode 100644 index 000000000..2fe8cfeeb --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/AccordionPicker.test.tsx.snap @@ -0,0 +1,181 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[AccordionPicker] renders correctly with items 1`] = ` + + + + + + Selected Item 1 + + + + + + + + +`; + +exports[`[AccordionPicker] shows "No Item available" when items list is empty 1`] = ` + + + + No Item available + + + +`; diff --git a/src/components/General/__tests__/__snapshots__/AmountInput.test.tsx.snap b/src/components/General/__tests__/__snapshots__/AmountInput.test.tsx.snap new file mode 100644 index 000000000..da1f20229 --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/AmountInput.test.tsx.snap @@ -0,0 +1,26 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[AmountInput] renders correctly with default props 1`] = ` + +`; + +exports[`[AmountInput] renders correctly with specific props 1`] = ` + +`; diff --git a/src/components/General/__tests__/__snapshots__/Avatar.test.tsx.snap b/src/components/General/__tests__/__snapshots__/Avatar.test.tsx.snap new file mode 100644 index 000000000..5fa560963 --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/Avatar.test.tsx.snap @@ -0,0 +1,163 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[Avatar] should apply styles correctly 1`] = ` + + + + + +`; + +exports[`[Avatar] should render correctly 1`] = ` + + + + + +`; + +exports[`[Avatar] should render correctly with a badge 1`] = ` + + + + + + + Badge + + + +`; + +exports[`[Avatar] should render correctly with loading state 1`] = ` + +`; diff --git a/src/components/General/__tests__/__snapshots__/Badge.test.tsx.snap b/src/components/General/__tests__/__snapshots__/Badge.test.tsx.snap new file mode 100644 index 000000000..01bd025f4 --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/Badge.test.tsx.snap @@ -0,0 +1,362 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[Badge] applies correct color based on the type 1`] = ` + + + XRPLNS + + +`; + +exports[`[Badge] renders correctly with a label 1`] = ` + + + Test Label + + +`; + +exports[`[Badge] renders correctly with default props 1`] = ` + +`; + +exports[`[Badge] renders correctly with different sizes 1`] = ` + +`; + +exports[`[Badge] renders correctly with different sizes 2`] = ` + +`; + +exports[`[Badge] renders correctly with different sizes 3`] = ` + +`; + +exports[`[Badge] renders correctly with specified type 1`] = ` + + + XRPLNS + + +`; diff --git a/src/components/General/__tests__/__snapshots__/Button.test.tsx.snap b/src/components/General/__tests__/__snapshots__/Button.test.tsx.snap index 14820a6cd..794d39b51 100644 --- a/src/components/General/__tests__/__snapshots__/Button.test.tsx.snap +++ b/src/components/General/__tests__/__snapshots__/Button.test.tsx.snap @@ -1,148 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Button Component should have TouchableNativeFeedback on android 1`] = ` - - - -`; - -exports[`Button Component should render with icon 1`] = ` - - - - - -`; - -exports[`Button Component should render with isDisabled 1`] = ` +exports[`[Button] should render with isDisabled 1`] = ` - -`; - -exports[`Button Component should render with label 1`] = ` - - - - label - - - -`; - -exports[`Button Component should render with loading 1`] = ` - - `; - -exports[`Button Component should render without issues 1`] = ` - - - -`; diff --git a/src/components/General/__tests__/__snapshots__/CheckBox.test.tsx.snap b/src/components/General/__tests__/__snapshots__/CheckBox.test.tsx.snap new file mode 100644 index 000000000..5112d5bf4 --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/CheckBox.test.tsx.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[CheckBox] renders correctly when checked 1`] = ` + + + + + + + + + Test Label + + + Test Label Small + + + Test Description + + + +`; + +exports[`[CheckBox] renders correctly when unchecked 1`] = ` + + + + + + + Test Label Unchecked + + + Test Label Small Unchecked + + + Test Description Unchecked + + + +`; diff --git a/src/components/General/__tests__/__snapshots__/DerivationPathInput.test.tsx.snap b/src/components/General/__tests__/__snapshots__/DerivationPathInput.test.tsx.snap new file mode 100644 index 000000000..377493020 --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/DerivationPathInput.test.tsx.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[DerivationPathInput] should match snapshot 1`] = ` + + + m/44'/144'/ + + + + '/ + + + + / + + + +`; diff --git a/src/components/General/__tests__/__snapshots__/ExpandableView.test.tsx.snap b/src/components/General/__tests__/__snapshots__/ExpandableView.test.tsx.snap new file mode 100644 index 000000000..bcd24ad04 --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/ExpandableView.test.tsx.snap @@ -0,0 +1,221 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[ExpandableView] renders correctly when collapsed 1`] = ` + + + + Expandable Title + + + + + Expandable Content + + + + + + +`; + +exports[`[ExpandableView] renders correctly when expanded 1`] = ` + + + + Expandable Title + + + + + Expandable Content + + + + + + +`; diff --git a/src/components/General/__tests__/__snapshots__/Header.test.tsx.snap b/src/components/General/__tests__/__snapshots__/Header.test.tsx.snap new file mode 100644 index 000000000..e7a838010 --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/Header.test.tsx.snap @@ -0,0 +1,835 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[Header] renders correctly with all components 1`] = ` + + + + Left + + + + + Center + + + + + Right + + + +`; + +exports[`[Header] renders correctly with backgroundColor 1`] = ` + + + + + +`; + +exports[`[Header] renders correctly with centerComponent 1`] = ` + + + + + Center + + + + +`; + +exports[`[Header] renders correctly with containerStyle 1`] = ` + + + + + +`; + +exports[`[Header] renders correctly with default props 1`] = ` + + + + + +`; + +exports[`[Header] renders correctly with leftComponent 1`] = ` + + + + Left + + + + + +`; + +exports[`[Header] renders correctly with rightComponent 1`] = ` + + + + + + Right + + + +`; diff --git a/src/components/General/__tests__/__snapshots__/HorizontalLine.test.tsx.snap b/src/components/General/__tests__/__snapshots__/HorizontalLine.test.tsx.snap new file mode 100644 index 000000000..b1c2e06f1 --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/HorizontalLine.test.tsx.snap @@ -0,0 +1,33 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[HorizontalLine] should render with custom props 1`] = ` + +`; + +exports[`[HorizontalLine] should render with default props 1`] = ` + +`; diff --git a/src/components/General/__tests__/__snapshots__/Icon.test.tsx.snap b/src/components/General/__tests__/__snapshots__/Icon.test.tsx.snap new file mode 100644 index 000000000..8763b363c --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/Icon.test.tsx.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[Icon] renders correctly with custom size 1`] = ` + +`; + +exports[`[Icon] renders correctly with custom style 1`] = ` + +`; + +exports[`[Icon] renders correctly with default props 1`] = ` + +`; diff --git a/src/components/General/__tests__/__snapshots__/LoadingIndicator.test.tsx.snap b/src/components/General/__tests__/__snapshots__/LoadingIndicator.test.tsx.snap new file mode 100644 index 000000000..3c875e498 --- /dev/null +++ b/src/components/General/__tests__/__snapshots__/LoadingIndicator.test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`[LoadingIndicator] renders while animating is false 1`] = ` + +`; + +exports[`[LoadingIndicator] renders with custom styles 1`] = ` + +`; + +exports[`[LoadingIndicator] renders with default props 1`] = ` + +`; + +exports[`[LoadingIndicator] renders with specified color (dark) 1`] = ` + +`; + +exports[`[LoadingIndicator] renders with specified color (light) 1`] = ` + +`; + +exports[`[LoadingIndicator] renders with specified size (large) 1`] = ` + +`; diff --git a/src/components/General/__tests__/__snapshots__/SearchBar.test.tsx.snap b/src/components/General/__tests__/__snapshots__/SearchBar.test.tsx.snap index 343815c41..468d899a8 100644 --- a/src/components/General/__tests__/__snapshots__/SearchBar.test.tsx.snap +++ b/src/components/General/__tests__/__snapshots__/SearchBar.test.tsx.snap @@ -379,7 +379,7 @@ exports[`[SearchBar] should show clear button on text enter 1`] = ` style={ { "alignSelf": "center", - "opacity": 1, + "opacity": 0, } } > diff --git a/src/components/General/index.ts b/src/components/General/index.ts index 7cf3e4406..a1f53f25e 100644 --- a/src/components/General/index.ts +++ b/src/components/General/index.ts @@ -11,7 +11,7 @@ export * from './InfoMessage'; export * from './Swiper'; export * from './AccordionPicker'; export * from './Footer'; -export * from './CardFlip'; + export * from './QRCode'; export * from './TextAvatar'; export * from './Icon'; @@ -23,7 +23,6 @@ export * from './ReadMore'; export * from './Button'; export * from './SegmentButtons'; -export * from './NativePaymentButton'; export * from './SwipeButton'; export * from './RadioButton'; export * from './RaisedButton'; @@ -48,3 +47,4 @@ export * from './WebView'; export * from './HeartBeatAnimation'; export * from './AnimatedDialog'; export * from './JsonTree'; +export * from './CountDown'; diff --git a/src/components/Modules/AccountElement/AccountElement.tsx b/src/components/Modules/AccountElement/AccountElement.tsx index ef68cd982..2bab1bd65 100644 --- a/src/components/Modules/AccountElement/AccountElement.tsx +++ b/src/components/Modules/AccountElement/AccountElement.tsx @@ -3,7 +3,7 @@ import { isEqual, isEmpty } from 'lodash'; import React, { Component } from 'react'; import { View, Text, ViewStyle, InteractionManager, TextStyle } from 'react-native'; -import { getAccountName, AccountNameType } from '@common/helpers/resolver'; +import AccountResolver, { AccountNameType } from '@common/helpers/resolver'; import { Navigator } from '@common/helpers/navigator'; import { AppScreens } from '@common/constants'; @@ -125,7 +125,7 @@ class AccountElement extends Component { }); } - getAccountName(address, tag) + AccountResolver.getAccountName(address, tag) .then((res) => { if (!isEmpty(res) && this.mounted) { this.setState( diff --git a/src/components/Modules/CurrencyElement/CurrencyElement.tsx b/src/components/Modules/CurrencyElement/CurrencyElement.tsx index 5d46a65e1..b366c1c39 100644 --- a/src/components/Modules/CurrencyElement/CurrencyElement.tsx +++ b/src/components/Modules/CurrencyElement/CurrencyElement.tsx @@ -7,7 +7,7 @@ import NetworkService from '@services/NetworkService'; import { WebLinks } from '@common/constants/endpoints'; -import { getAccountName, AccountNameType } from '@common/helpers/resolver'; +import AccountResolver, { AccountNameType } from '@common/helpers/resolver'; import { NormalizeCurrencyCode } from '@common/utils/monetary'; import { Images } from '@common/helpers/images'; @@ -72,7 +72,7 @@ class CurrencyElement extends Component { }); } - getAccountName(issuer) + AccountResolver.getAccountName(issuer) .then((res) => { if (!isEmpty(res)) { this.setState({ diff --git a/src/components/Modules/EventsList/EventListItems/LedgerObject.tsx b/src/components/Modules/EventsList/EventListItems/LedgerObject.tsx index f2c608de7..eb92d460a 100644 --- a/src/components/Modules/EventsList/EventListItems/LedgerObject.tsx +++ b/src/components/Modules/EventsList/EventListItems/LedgerObject.tsx @@ -12,7 +12,8 @@ import { ExplainerFactory } from '@common/libs/ledger/factory'; import { MutationsMixinType } from '@common/libs/ledger/mixin/types'; import { Navigator } from '@common/helpers/navigator'; -import { AccountNameType, getAccountName } from '@common/helpers/resolver'; + +import AccountResolver, { AccountNameType } from '@common/helpers/resolver'; import { TouchableDebounce } from '@components/General'; @@ -121,7 +122,7 @@ class LedgerObjectItem extends Component { try { // get participant details - const resp = await getAccountName(otherParty.address, otherParty.tag); + const resp = await AccountResolver.getAccountName(otherParty.address, otherParty.tag); if (!isEmpty(resp) && this.mounted) { this.setState({ explainer, diff --git a/src/components/Modules/EventsList/EventListItems/Transaction.tsx b/src/components/Modules/EventsList/EventListItems/Transaction.tsx index 8aa7cb52e..7bc0bb3bf 100644 --- a/src/components/Modules/EventsList/EventListItems/Transaction.tsx +++ b/src/components/Modules/EventsList/EventListItems/Transaction.tsx @@ -13,7 +13,7 @@ import { LedgerObjects } from '@common/libs/ledger/objects/types'; import { AccountModel } from '@store/models'; import { Navigator } from '@common/helpers/navigator'; -import { AccountNameType, getAccountName } from '@common/helpers/resolver'; +import AccountResolver, { AccountNameType } from '@common/helpers/resolver'; import { AppScreens } from '@common/constants'; @@ -121,7 +121,7 @@ class TransactionItem extends Component { try { // get participant details - const resp = await getAccountName(otherParty.address, otherParty.tag); + const resp = await AccountResolver.getAccountName(otherParty.address, otherParty.tag); if (!isEmpty(resp) && this.mounted) { this.setState({ explainer, diff --git a/src/components/Modules/MonetizationElement/MonetizationElement.tsx b/src/components/Modules/MonetizationElement/MonetizationElement.tsx index 20d9d2f11..057babcdb 100644 --- a/src/components/Modules/MonetizationElement/MonetizationElement.tsx +++ b/src/components/Modules/MonetizationElement/MonetizationElement.tsx @@ -8,7 +8,7 @@ import { ProfileRepository, UserInteractionRepository } from '@store/repositorie import { MonetizationStatus } from '@store/types'; import { InteractionTypes } from '@store/models/objects/userInteraction'; -import { PurchaseProductOverlayProps } from '@screens/Overlay/PurchaseProduct'; +import { PurchaseProductModalProps } from '@screens/Modal/PurchaseProduct'; import { Button, RaisedButton } from '@components/General'; @@ -94,7 +94,7 @@ class MonetizationElement extends PureComponent { purchaseProduct = () => { const { productForPurchase, monetizationType } = this.state; - Navigator.showOverlay(AppScreens.Overlay.PurchaseProduct, { + Navigator.showModal(AppScreens.Modal.PurchaseProduct, { productId: productForPurchase!, productDescription: monetizationType!, onSuccessPurchase: this.onSuccessPurchase, diff --git a/src/components/Modules/MutationWidgets/ActionButtons.tsx b/src/components/Modules/MutationWidgets/ActionButtons.tsx index 0b4c6fba5..330291ab5 100644 --- a/src/components/Modules/MutationWidgets/ActionButtons.tsx +++ b/src/components/Modules/MutationWidgets/ActionButtons.tsx @@ -12,6 +12,7 @@ import NetworkService from '@services/NetworkService'; import { Payload } from '@common/libs/payload'; import { LedgerEntryTypes, TransactionTypes } from '@common/libs/ledger/types/enums'; +import { AmountParser } from '@common/libs/ledger/parser/common'; import { TransactionJson } from '@common/libs/ledger/types/transaction'; import { Button } from '@components/General'; @@ -31,7 +32,9 @@ enum ActionTypes { NEW_PAYMENT = 'NEW_PAYMENT', CANCEL_OFFER = 'CANCEL_OFFER', ACCEPT_NFTOKEN_OFFER = 'ACCEPT_NFTOKEN_OFFER', + ACCEPT_URITOKEN_OFFER = 'ACCEPT_URITOKEN_OFFER', SELL_NFTOKEN = 'SELL_NFTOKEN', + SELL_URITOKEN = 'SELL_URITOKEN', CANCEL_ESCROW = 'CANCEL_ESCROW', FINISH_ESCROW = 'FINISH_ESCROW', CANCEL_CHECK = 'CANCEL_CHECK', @@ -58,6 +61,10 @@ const ActionButton: React.FC<{ actionType: ActionTypes; onPress: (actionType: Ac return { label: Localize.t('events.acceptOffer'), secondary: true }; case ActionTypes.SELL_NFTOKEN: return { label: Localize.t('events.sellMyNFT'), secondary: true }; + case ActionTypes.SELL_URITOKEN: + return { label: Localize.t('events.sellMyNFT'), secondary: true }; + case ActionTypes.ACCEPT_URITOKEN_OFFER: + return { label: Localize.t('events.acceptOffer'), secondary: true }; case ActionTypes.CANCEL_ESCROW: return { label: Localize.t('events.cancelEscrow'), secondary: true }; case ActionTypes.FINISH_ESCROW: @@ -143,12 +150,23 @@ class ActionButtons extends PureComponent { if (item.Owner === account.address) { availableActions.push(ActionTypes.CANCEL_OFFER); } else if (!item.Destination || item.Destination === account.address) { - if (item.Flags?.tfSellToken) { + if (item.Flags?.lsfSellNFToken) { availableActions.push(ActionTypes.ACCEPT_NFTOKEN_OFFER); } else { availableActions.push(ActionTypes.SELL_NFTOKEN); } } + break; + case LedgerEntryTypes.URIToken: + case TransactionTypes.URITokenMint: + if (item.Destination) { + if (item.Destination === account.address) { + availableActions.push(ActionTypes.ACCEPT_URITOKEN_OFFER); + } else { + availableActions.push(ActionTypes.CANCEL_OFFER); + } + } + break; case LedgerEntryTypes.Escrow: if (item.isExpired) { @@ -232,6 +250,11 @@ class ActionButtons extends PureComponent { TransactionType: TransactionTypes.NFTokenCancelOffer, NFTokenOffers: [item.Index], }); + } else if (item.Type === LedgerEntryTypes.URIToken) { + Object.assign(craftedTxJson, { + TransactionType: TransactionTypes.URITokenCancelSellOffer, + URITokenID: item.URITokenID, + }); } break; case ActionTypes.ACCEPT_NFTOKEN_OFFER: @@ -239,8 +262,8 @@ class ActionButtons extends PureComponent { if (item.Type === LedgerEntryTypes.NFTokenOffer) { Object.assign(craftedTxJson, { TransactionType: TransactionTypes.NFTokenAcceptOffer, - NFTokenSellOffer: item.Flags?.tfSellToken ? item.Index : undefined, - NFTokenBuyOffer: !item.Flags?.tfSellToken ? item.Index : undefined, + NFTokenSellOffer: item.Flags?.lsfSellNFToken ? item.Index : undefined, + NFTokenBuyOffer: !item.Flags?.lsfSellNFToken ? item.Index : undefined, }); } break; @@ -287,6 +310,18 @@ class ActionButtons extends PureComponent { }); } break; + case ActionTypes.ACCEPT_URITOKEN_OFFER: + if (item.Type === LedgerEntryTypes.URIToken || item.Type === TransactionTypes.URITokenMint) { + Object.assign(craftedTxJson, { + TransactionType: TransactionTypes.URITokenBuy, + URITokenID: item.URITokenID, + Amount: + item.Amount!.currency === NetworkService.getNativeAsset() + ? new AmountParser(item.Amount!.value, false).nativeToDrops().toString() + : item.Amount, + }); + } + break; default: break; } diff --git a/src/components/Modules/MutationWidgets/AssetsMutations.tsx b/src/components/Modules/MutationWidgets/AssetsMutations.tsx index b6426a4a2..5b7d1e75f 100644 --- a/src/components/Modules/MutationWidgets/AssetsMutations.tsx +++ b/src/components/Modules/MutationWidgets/AssetsMutations.tsx @@ -3,6 +3,7 @@ import { View } from 'react-native'; import { AmountText, Icon } from '@components/General'; import { NFTokenElement } from '@components/Modules/NFTokenElement'; +import { URITokenElement } from '@components/Modules/URITokenElement'; import { AssetDetails, AssetTypes, MonetaryFactorType, MonetaryStatus } from '@common/libs/ledger/factory/types'; import { BalanceChangeType, OperationActions } from '@common/libs/ledger/parser/types'; @@ -66,6 +67,14 @@ class AssetsMutations extends PureComponent { containerStyle={styles.nfTokenContainer} /> ); + case AssetTypes.URIToken: + return ( + + ); default: return null; } diff --git a/src/components/Modules/MutationWidgets/ReserveChange.tsx b/src/components/Modules/MutationWidgets/ReserveChange.tsx index 2951e2789..6d054227b 100644 --- a/src/components/Modules/MutationWidgets/ReserveChange.tsx +++ b/src/components/Modules/MutationWidgets/ReserveChange.tsx @@ -1,5 +1,7 @@ +import BigNumber from 'bignumber.js'; + import React, { PureComponent } from 'react'; -import { Text, View } from 'react-native'; +import { InteractionManager, Text, View } from 'react-native'; import { Navigator } from '@common/helpers/navigator'; import { AppScreens } from '@common/constants'; @@ -21,8 +23,53 @@ import styles from './styles'; /* Types ==================================================================== */ import { Props } from './types'; +interface State { + value?: string; + action?: OperationActions; +} + /* Component ==================================================================== */ -class ReserveChange extends PureComponent { +class ReserveChange extends PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + value: undefined, + action: undefined, + }; + } + + componentDidMount() { + InteractionManager.runAfterInteractions(this.setOwnerReserveChanges); + } + + setOwnerReserveChanges = () => { + const { item } = this.props; + + let ownerReserveChanges: OwnerCountChangeType | undefined; + + switch (item.InstanceType) { + case InstanceTypes.LedgerObject: + ownerReserveChanges = this.getLedgerObjectChanges(); + break; + case InstanceTypes.GenuineTransaction: + case InstanceTypes.FallbackTransaction: + ownerReserveChanges = this.getTransactionChanges(); + break; + default: + break; + } + + if (ownerReserveChanges) { + this.setState({ + value: new BigNumber(NetworkService.getNetworkReserve().OwnerReserve) + .multipliedBy(ownerReserveChanges.value) + .toString(), + action: ownerReserveChanges.action, + }); + } + }; + showBalanceExplain = () => { const { account } = this.props; @@ -38,8 +85,11 @@ class ReserveChange extends PureComponent { getLedgerObjectChanges = (): OwnerCountChangeType | undefined => { const { item, account } = this.props; - // ignore for incoming NFTokenOffers - if (item.Type === LedgerEntryTypes.NFTokenOffer && item.Owner !== account.address) { + // ignore for incoming NFTokenOffers and URITokenOffers + if ( + (item.Type === LedgerEntryTypes.NFTokenOffer || item.Type === LedgerEntryTypes.URIToken) && + item.Owner !== account.address + ) { return undefined; } @@ -58,23 +108,9 @@ class ReserveChange extends PureComponent { }; render() { - const { item } = this.props; - - let changes; - - switch (item.InstanceType) { - case InstanceTypes.LedgerObject: - changes = this.getLedgerObjectChanges(); - break; - case InstanceTypes.GenuineTransaction: - case InstanceTypes.FallbackTransaction: - changes = this.getTransactionChanges(); - break; - default: - break; - } + const { value, action } = this.state; - if (!changes) { + if (!value || !action) { return null; } @@ -82,7 +118,7 @@ class ReserveChange extends PureComponent { @@ -90,21 +126,21 @@ class ReserveChange extends PureComponent { - - {changes.action === OperationActions.INC + + {action === OperationActions.INC ? Localize.t('events.thisTransactionIncreaseAccountReserve', { - ownerReserve: Number(changes.value) * NetworkService.getNetworkReserve().OwnerReserve, + ownerReserve: value, nativeAsset: NetworkService.getNativeAsset(), }) : Localize.t('events.thisTransactionDecreaseAccountReserve', { - ownerReserve: Number(changes.value) * NetworkService.getNetworkReserve().OwnerReserve, + ownerReserve: value, nativeAsset: NetworkService.getNativeAsset(), })}