diff --git a/android/app/src/main/java/com/bitpay/wallet/CrashTestModule.java b/android/app/src/main/java/com/bitpay/wallet/CrashTestModule.java new file mode 100644 index 000000000..81e45aa29 --- /dev/null +++ b/android/app/src/main/java/com/bitpay/wallet/CrashTestModule.java @@ -0,0 +1,29 @@ +package com.bitpay.wallet; + +import android.os.Handler; +import android.os.Looper; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; + +public class CrashTestModule extends ReactContextBaseJavaModule { + public CrashTestModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "CrashTest"; + } + + @ReactMethod + public void crash() { + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + throw new RuntimeException("BitPay test: native crash (Sentry)"); + } + }); + } +} diff --git a/android/app/src/main/java/com/bitpay/wallet/CrashTestPackage.java b/android/app/src/main/java/com/bitpay/wallet/CrashTestPackage.java new file mode 100644 index 000000000..2bbe09fca --- /dev/null +++ b/android/app/src/main/java/com/bitpay/wallet/CrashTestPackage.java @@ -0,0 +1,27 @@ +package com.bitpay.wallet; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class CrashTestPackage implements ReactPackage { + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new CrashTestModule(reactContext)); + + return modules; + } +} diff --git a/android/app/src/main/java/com/bitpay/wallet/MainApplication.kt b/android/app/src/main/java/com/bitpay/wallet/MainApplication.kt index 6569c29c4..bbceb6967 100644 --- a/android/app/src/main/java/com/bitpay/wallet/MainApplication.kt +++ b/android/app/src/main/java/com/bitpay/wallet/MainApplication.kt @@ -28,6 +28,7 @@ class MainApplication : Application(), ReactApplication { add(SilentPushPackage()) add(TimerPackage()) add(InAppMessagePackage()) + add(CrashTestPackage()) }, ) } diff --git a/ios/BitPayApp.xcodeproj/project.pbxproj b/ios/BitPayApp.xcodeproj/project.pbxproj index 8665f7982..ea26b4258 100644 --- a/ios/BitPayApp.xcodeproj/project.pbxproj +++ b/ios/BitPayApp.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 5BCF1C352740B8B2006D872E /* BootSplash.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5BCF1C342740B8B2006D872E /* BootSplash.storyboard */; }; 5BEA4BF02838402700AB2121 /* RCTPaymentPass.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5BEA4BEF2838402700AB2121 /* RCTPaymentPass.mm */; }; 6D615C4929725FD70063DB8E /* RCTTimer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6D615C4729725FD70063DB8E /* RCTTimer.mm */; }; + CA5471E01A2B3C4D5E6F7001 /* RCTCrashTest.mm in Sources */ = {isa = PBXBuildFile; fileRef = CA5471E01A2B3C4D5E6F7002 /* RCTCrashTest.mm */; }; 6D7C95E72E9007E200F0D481 /* RCTPushPermissionManager.mm in Sources */ = {isa = PBXBuildFile; fileRef = 6D7C95E62E9007E200F0D481 /* RCTPushPermissionManager.mm */; }; 71BABDA4524A43E98A190C87 /* Archivo-Light.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 6D649751C2644FDCA18A5547 /* Archivo-Light.ttf */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; @@ -55,6 +56,8 @@ 5D5D40A7E7B5265D652187F9 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = BitPayApp/PrivacyInfo.xcprivacy; sourceTree = ""; }; 6D615C4729725FD70063DB8E /* RCTTimer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTTimer.mm; sourceTree = ""; }; 6D615C4829725FD70063DB8E /* RCTTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTTimer.h; sourceTree = ""; }; + CA5471E01A2B3C4D5E6F7002 /* RCTCrashTest.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTCrashTest.mm; sourceTree = ""; }; + CA5471E01A2B3C4D5E6F7003 /* RCTCrashTest.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTCrashTest.h; sourceTree = ""; }; 6D649751C2644FDCA18A5547 /* Archivo-Light.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Light.ttf"; path = "../assets/fonts/Archivo-Light.ttf"; sourceTree = ""; }; 6D7C95E62E9007E200F0D481 /* RCTPushPermissionManager.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = RCTPushPermissionManager.mm; sourceTree = ""; }; 715670CB346B45A7A9EC2044 /* Archivo-Bold.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Archivo-Bold.ttf"; path = "../assets/fonts/Archivo-Bold.ttf"; sourceTree = ""; }; @@ -98,6 +101,8 @@ 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, 6D615C4829725FD70063DB8E /* RCTTimer.h */, 6D615C4729725FD70063DB8E /* RCTTimer.mm */, + CA5471E01A2B3C4D5E6F7003 /* RCTCrashTest.h */, + CA5471E01A2B3C4D5E6F7002 /* RCTCrashTest.mm */, 5B31AAFC2786381C002765E1 /* RCTDosh.h */, 5B31AAFD2786387B002765E1 /* RCTDosh.mm */, 5B31AB5E2786515A002765E1 /* Dosh.swift */, @@ -372,6 +377,7 @@ 6D7C95E72E9007E200F0D481 /* RCTPushPermissionManager.mm in Sources */, 5BEA4BF02838402700AB2121 /* RCTPaymentPass.mm in Sources */, 6D615C4929725FD70063DB8E /* RCTTimer.mm in Sources */, + CA5471E01A2B3C4D5E6F7001 /* RCTCrashTest.mm in Sources */, ECCB92782860B25D00F9CF8B /* SilentPushEvent.mm in Sources */, 5B31AB5F2786515A002765E1 /* Dosh.swift in Sources */, 52C9714B9004BCE190EA469D /* BundleHash.swift in Sources */, diff --git a/ios/RCTCrashTest.h b/ios/RCTCrashTest.h new file mode 100644 index 000000000..a3a17c4d6 --- /dev/null +++ b/ios/RCTCrashTest.h @@ -0,0 +1,6 @@ +#import +#import + +@interface RCTCrashTest : NSObject + +@end diff --git a/ios/RCTCrashTest.mm b/ios/RCTCrashTest.mm new file mode 100644 index 000000000..ccef01956 --- /dev/null +++ b/ios/RCTCrashTest.mm @@ -0,0 +1,15 @@ +#import "RCTCrashTest.h" + +@implementation RCTCrashTest + +RCT_EXPORT_MODULE(CrashTest); + +RCT_EXPORT_METHOD(crash) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [NSException raise:@"BitPayTestCrash" + format:@"BitPay test: native crash (Sentry)"]; + }); +} + +@end diff --git a/src/lib/crash-test/index.ts b/src/lib/crash-test/index.ts new file mode 100644 index 000000000..ee359b36e --- /dev/null +++ b/src/lib/crash-test/index.ts @@ -0,0 +1,27 @@ +import {NativeModules, Platform} from 'react-native'; + +/** + * DEV-only helpers to validate crash reporting (Sentry). + */ + +interface CrashTestNativeModule { + crash(): void; +} + +const CrashTest = NativeModules.CrashTest as CrashTestNativeModule | undefined; + +export const triggerNativeCrash = (): void => { + if (!CrashTest?.crash) { + throw new Error( + `CrashTest native module unavailable on ${Platform.OS}. ` + + 'Rebuild the native app (the JS bundle alone is not enough).', + ); + } + CrashTest.crash(); +}; + +export const triggerJsCrash = (): void => { + setTimeout(() => { + throw new Error('BitPay test: JS crash (Sentry)'); + }, 0); +}; diff --git a/src/navigation/tabs/settings/components/About.tsx b/src/navigation/tabs/settings/components/About.tsx index aa24316f9..518af667c 100644 --- a/src/navigation/tabs/settings/components/About.tsx +++ b/src/navigation/tabs/settings/components/About.tsx @@ -20,6 +20,7 @@ import AngleRight from '../../../../../assets/img/angle-right.svg'; import {GIT_COMMIT_HASH} from '@env'; import {Analytics} from '../../../../store/analytics/analytics.effects'; import {useAppDispatch} from '../../../../utils/hooks'; +import {triggerJsCrash, triggerNativeCrash} from '../../../../lib/crash-test'; interface LinkSetting { key: string; @@ -124,6 +125,26 @@ const About = () => { ); })} + + {__DEV__ ? ( + <> +
+ + {t('Crash Test (DEV)')} + + + + + + + ) : null} ); };