diff --git a/.changeset/friendly-masks-vanish.md b/.changeset/friendly-masks-vanish.md new file mode 100644 index 00000000..2953f0d9 --- /dev/null +++ b/.changeset/friendly-masks-vanish.md @@ -0,0 +1,5 @@ +--- +"@react-native-documents/viewer": major +--- + +fix: viewer module errorCodes diff --git a/packages/document-viewer/android/src/main/java/com/reactnativedocumentviewer/RNDocumentViewerModule.kt b/packages/document-viewer/android/src/main/java/com/reactnativedocumentviewer/RNDocumentViewerModule.kt index b757fdc2..85b34de2 100644 --- a/packages/document-viewer/android/src/main/java/com/reactnativedocumentviewer/RNDocumentViewerModule.kt +++ b/packages/document-viewer/android/src/main/java/com/reactnativedocumentviewer/RNDocumentViewerModule.kt @@ -19,21 +19,18 @@ class RNDocumentViewerModule(reactContext: ReactApplicationContext) : NativeDocu permissions: String, mimeType: String?, title: String?, + androidApplicationId: String?, presentation: String?, promise: Promise ) { - val currentActivity = reactApplicationContext.currentActivity - if (currentActivity == null) { - rejectWithNullActivity(promise) - return - } + val currentActivity = reactApplicationContext.currentActivity ?: return rejectWithNullActivity(promise) if (BuildConfig.DEBUG && mimeType != null && !MimeTypeMap.getSingleton().hasMimeType(mimeType)) { RNLog.w( reactApplicationContext, "$mimeType appears to be an unusual mime type, are you sure it's correct?") } try { - val (uriToOpen, intentFlags) = constructUri(bookmarkOrUri, permissions) + val (uriToOpen, intentFlags) = constructUri(bookmarkOrUri, permissions, androidApplicationId) // grantUriPermission is not needed (for file uris WE OWN), we're using the Flags // on a Uri returned by FileProvider.getUriForFile() @@ -53,7 +50,7 @@ class RNDocumentViewerModule(reactContext: ReactApplicationContext) : NativeDocu } } - private fun constructUri(bookmarkOrUri: String, permissions: String): UriWithFlags { + private fun constructUri(bookmarkOrUri: String, permissions: String, androidApplicationId: String?): UriWithFlags { val flags = when (permissions) { "write" -> Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION else -> Intent.FLAG_GRANT_READ_URI_PERMISSION @@ -61,9 +58,11 @@ class RNDocumentViewerModule(reactContext: ReactApplicationContext) : NativeDocu return if (bookmarkOrUri.startsWith("content://")) { UriWithFlags(Uri.parse(bookmarkOrUri), flags) } else if (bookmarkOrUri.startsWith("file://")) { + // Package name may not be the same as applicationId but usually is. + // Also see document-viewer/android/src/main/AndroidManifest.xml + val applicationId = androidApplicationId ?: reactApplicationContext.packageName + val authority = "$applicationId.reactnativedocumentviewer.fileprovider" val uri = Uri.parse(bookmarkOrUri) - // TODO package name may not be the same as applicationId. Also see document-viewer/android/src/main/AndroidManifest.xml - val authority = reactApplicationContext.packageName + ".reactnativedocumentviewer.fileprovider" val uriPath = uri.path ?: throw IllegalArgumentException("file:// uri must have a path") val fileUri = FileProvider.getUriForFile( reactApplicationContext, @@ -81,10 +80,10 @@ class RNDocumentViewerModule(reactContext: ReactApplicationContext) : NativeDocu companion object { fun rejectWithNullActivity(promise: Promise) { - promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "activity is null") + promise.reject(NULL_PRESENTER, "current activity is null") } - private const val E_ACTIVITY_DOES_NOT_EXIST = "ACTIVITY_DOES_NOT_EXIST" + private const val NULL_PRESENTER = "NULL_PRESENTER" private const val UNABLE_TO_OPEN_FILE_TYPE = "UNABLE_TO_OPEN_FILE_TYPE" } } diff --git a/packages/document-viewer/android/src/paper/java/com/reactnativedocumentviewer/NativeDocumentViewerSpec.java b/packages/document-viewer/android/src/paper/java/com/reactnativedocumentviewer/NativeDocumentViewerSpec.java index 9c7d3bc2..f708dc29 100644 --- a/packages/document-viewer/android/src/paper/java/com/reactnativedocumentviewer/NativeDocumentViewerSpec.java +++ b/packages/document-viewer/android/src/paper/java/com/reactnativedocumentviewer/NativeDocumentViewerSpec.java @@ -34,5 +34,5 @@ public NativeDocumentViewerSpec(ReactApplicationContext reactContext) { @ReactMethod @DoNotStrip - public abstract void viewDocument(String bookmarkOrUri, String permissions, @Nullable String mimeType, @Nullable String title, @Nullable String presentationStyle, Promise promise); + public abstract void viewDocument(String bookmarkOrUri, String permissions, @Nullable String mimeType, @Nullable String title, @Nullable String androidApplicationId, @Nullable String presentationStyle, Promise promise); } diff --git a/packages/document-viewer/ios/RNDocumentViewer.mm b/packages/document-viewer/ios/RNDocumentViewer.mm index b24aa67a..bbef0c65 100644 --- a/packages/document-viewer/ios/RNDocumentViewer.mm +++ b/packages/document-viewer/ios/RNDocumentViewer.mm @@ -3,9 +3,10 @@ #import "RNDocumentViewer.h" #import #import "RNDPreviewController.h" -// for UIModalPresentationStyle conversion -// remove after https://github.com/facebook/react-native/commit/2d547a3252b328251e49dabfeec85f8d46c85411 is released -#import +#import + +static NSString * const RNDocViewerErrorUnableToOpenFileType = @"UNABLE_TO_OPEN_FILE_TYPE"; +static NSString * const RNDocViewerErrorNullPresenter = @"NULL_PRESENTER"; @interface RNDocumentViewer () @property(nonatomic, nullable) NSURL *presentedUrl; @@ -24,6 +25,7 @@ + (BOOL)requiresMainQueueSetup { permissions:(NSString *)permissions mimeType:(NSString *)mimeType title:(NSString *)title + androidApplicationId:(NSString *)androidApplicationId presentationStyle:(NSString *)presentationStyle resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { @@ -34,10 +36,10 @@ + (BOOL)requiresMainQueueSetup { [self presentPreview:title restoredURL:restoredURL presentationStyle:_presentationStyle resolve:resolve reject:reject]; } else { NSData *bookmarkData = [[NSData alloc] initWithBase64EncodedString:bookmarkOrUri options:0]; - + NSError *error = nil; BOOL isStale = NO; - + NSURL *restoredURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithoutUI relativeToURL:nil @@ -50,7 +52,6 @@ + (BOOL)requiresMainQueueSetup { reject(@"RNDocViewer", @"Bookmark was resolved but it's stale", nil); return; } - if ([restoredURL startAccessingSecurityScopedResource]) { [self presentPreview:title restoredURL:restoredURL presentationStyle:_presentationStyle resolve:resolve reject:reject]; } else { @@ -62,7 +63,7 @@ + (BOOL)requiresMainQueueSetup { } } -- (void)presentPreview:(NSString *)title +- (void)presentPreview:(NSString *)title restoredURL:(NSURL *)restoredURL presentationStyle:(UIModalPresentationStyle) presentationStyle resolve:(RCTPromiseResolveBlock)resolve @@ -75,12 +76,16 @@ - (void)presentPreview:(NSString *)title controller.delegate = self; if ([QLPreviewController canPreviewItem:item]) { - [RCTPresentedViewController() presentViewController:controller animated:YES completion:^{ + UIViewController* presenter = RCTPresentedViewController(); + if (presenter) { + [presenter presentViewController:controller animated:YES completion:nil]; resolve([NSNull null]); - }]; + } else { + reject(RNDocViewerErrorNullPresenter, @"RCTPresentedViewController was nil", nil); + } } else { [self.presentedUrl stopAccessingSecurityScopedResource]; - reject(@"UNABLE_TO_OPEN_FILE_TYPE", @"unsupported file", nil); + reject(RNDocViewerErrorUnableToOpenFileType, @"unsupported file", nil); } }); } diff --git a/packages/document-viewer/src/errors.ts b/packages/document-viewer/src/errors.ts index 0e3562ab..05acce2e 100644 --- a/packages/document-viewer/src/errors.ts +++ b/packages/document-viewer/src/errors.ts @@ -1,23 +1,21 @@ -export interface NativeModuleError extends Error { - code: string -} - -const OPERATION_CANCELED = 'OPERATION_CANCELED' -const IN_PROGRESS = 'ASYNC_OP_IN_PROGRESS' const UNABLE_TO_OPEN_FILE_TYPE = 'UNABLE_TO_OPEN_FILE_TYPE' +const NULL_PRESENTER = 'NULL_PRESENTER' export const errorCodes = Object.freeze({ - OPERATION_CANCELED, - IN_PROGRESS, UNABLE_TO_OPEN_FILE_TYPE, + NULL_PRESENTER, }) +type ErrorCodes = (typeof errorCodes)[keyof typeof errorCodes] + +export interface NativeModuleError extends Error { + code: ErrorCodes | (string & {}) +} + /** * TypeScript helper to check if an object has the `code` property. * This is used to avoid `as` casting when you access the `code` property on errors returned by the module. */ -export const isErrorWithCode = (error: any): error is NativeModuleError => { - // to account for https://github.com/facebook/react-native/issues/41950 - const isNewArchErrorIOS = typeof error === 'object' && error != null - return (error instanceof Error || isNewArchErrorIOS) && 'code' in error +export const isErrorWithCode = (error: unknown): error is NativeModuleError => { + return error instanceof Error && 'code' in error } diff --git a/packages/document-viewer/src/index.ts b/packages/document-viewer/src/index.ts index e69384e1..02b567c3 100644 --- a/packages/document-viewer/src/index.ts +++ b/packages/document-viewer/src/index.ts @@ -17,23 +17,28 @@ export type PresentationStyle = export type BaseOptions = { /** * Android only: The type of permission to grant to the receiving app that will open the document. - * This only has effect if you're viewing a file that lives in the app's sandboxed storage. + * This only has an effect if you're viewing a file that lives in the app's sandboxed storage. */ grantPermissions?: 'read' | 'write' /** * iOS only: The title to display in the header of the document viewer. - * If not provided, the filename will be used. + * @default the file name. */ headerTitle?: string /** - * Optional, but recommended: the mimetype of the document. This will help the Android OS to find the right app(s) to open the document. + * Optional, but strongly recommended: the mimetype of the document. This helps the Android OS to find the right app(s) to open the document. */ mimeType?: string - /** - * iOS only - Controls how the picker is presented, e.g. on an iPad you may want to present it fullscreen. Defaults to `pageSheet`. + * iOS only - Controls how the picker is presented, e.g. on an iPad you may want to present it fullscreen. + * @default `pageSheet`. * */ presentationStyle?: PresentationStyle + /** + * Android only - Optional, only provide a value if `viewDocument` rejects with `IllegalArgumentException`. Represents the unique identifier for an Android application. + * @default application package name, which usually is the same as the application id. + */ + androidApplicationId?: string } /** @@ -74,6 +79,7 @@ export function viewDocument(data: ViewDocumentOptions): Promise { data?.grantPermissions ?? 'read', data?.mimeType, data?.headerTitle, + data?.androidApplicationId, data?.presentationStyle, ) } diff --git a/packages/document-viewer/src/spec/NativeDocumentViewer.ts b/packages/document-viewer/src/spec/NativeDocumentViewer.ts index 433087c2..f5623ddf 100644 --- a/packages/document-viewer/src/spec/NativeDocumentViewer.ts +++ b/packages/document-viewer/src/spec/NativeDocumentViewer.ts @@ -8,6 +8,7 @@ export interface Spec extends TurboModule { permissions: string, mimeType?: string, title?: string, + androidApplicationId?: string, presentationStyle?: string, ): Promise }