From ebaaf44e473817db9c27a6d0c40ce900a80ccd3c Mon Sep 17 00:00:00 2001 From: David Estes Date: Fri, 28 Aug 2020 12:56:12 -0700 Subject: [PATCH] Update based on review and IR feedback --- .../en.lproj/Localizable.strings | 3 + Stripe/STPAddCardViewController.m | 14 +++- Stripe/STPCardScanner.h | 11 +++- Stripe/STPCardScanner.m | 66 +++++++++---------- Stripe/STPCardScannerTableViewCell.m | 2 +- 5 files changed, 59 insertions(+), 37 deletions(-) diff --git a/Stripe/Resources/Localizations/en.lproj/Localizable.strings b/Stripe/Resources/Localizations/en.lproj/Localizable.strings index 4982e97c727..f163b59bf25 100644 --- a/Stripe/Resources/Localizations/en.lproj/Localizable.strings +++ b/Stripe/Resources/Localizations/en.lproj/Localizable.strings @@ -199,6 +199,9 @@ /* Error when 3DS2 authentication timed out. */ "Timed out authenticating your payment method -- try again" = "Timed out authenticating your payment method -- try again"; +/* Error when the user hasn't allowed the current app to access the camera when scanning a payment card. 'Settings' is the localized name of the iOS Settings app. */ +"To scan your card, you'll need to allow access to your camera in Settings." = "To scan your card, you'll need to allow access to your camera in Settings."; + /* Default missing source type label */ "Unknown" = "Unknown"; diff --git a/Stripe/STPAddCardViewController.m b/Stripe/STPAddCardViewController.m index ad56d6590e8..acfe1e62988 100644 --- a/Stripe/STPAddCardViewController.m +++ b/Stripe/STPAddCardViewController.m @@ -231,12 +231,16 @@ - (void)setIsScanning:(BOOL)isScanning { self.cardHeaderView.button.enabled = !isScanning; NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:STPPaymentCardScannerSection]; + [self.tableView beginUpdates]; if (isScanning) { [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; - [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; } else { [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } + [self.tableView endUpdates]; + if (isScanning) { + [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; + } [self updateInputAccessoryVisiblity]; } @@ -600,9 +604,14 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id -- (void)cardScanner:(STPCardScanner *)scanner didFinishWithCardParams:(nullable STPPaymentMethodCardParams *)cardParams; +- (void)cardScanner:(STPCardScanner *)scanner didFinishWithCardParams:(nullable STPPaymentMethodCardParams *)cardParams error:(nullable NSError *)error; @end API_AVAILABLE(ios(13.0)) diff --git a/Stripe/STPCardScanner.m b/Stripe/STPCardScanner.m index 8f99ff7b24c..fc865ebbc0d 100644 --- a/Stripe/STPCardScanner.m +++ b/Stripe/STPCardScanner.m @@ -15,6 +15,8 @@ #import "STPCardValidator+Private.h" #import "STPPaymentMethodCardParams.h" #import "STPStringUtils.h" +#import "STPLocalizationUtils.h" +#import "StripeError.h" // The number of successful scans required for both card number and expiration date before returning a result. static const NSUInteger kSTPCardScanningMinimumValidScans = 2; @@ -23,6 +25,8 @@ // Once one successful scan is found, we'll stop scanning after this many seconds. static const NSTimeInterval kSTPCardScanningTimeout = 1.0; +NSString * const STPCardScannerErrorDomain = @"STPCardScannerErrorDomain"; + @interface STPCardScanner () @property (nonatomic, weak) iddelegate; @property (nonatomic, strong) AVCaptureDevice *captureDevice; @@ -39,7 +43,7 @@ @interface STPCardScanner () @property (atomic) BOOL didTimeout; @property (atomic) BOOL timeoutStarted; -@property (atomic) UIDeviceOrientation _deviceOrientation; +@property (atomic) UIDeviceOrientation _stp_deviceOrientation; @property (atomic) AVCaptureVideoOrientation videoOrientation; @property (atomic) CGImagePropertyOrientation textOrientation; @@ -70,17 +74,15 @@ + (BOOL)cardScanningAvailable { cameraHasUsageDescription = YES; } }); - BOOL cameraAllowed = YES; - AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; - switch (status) { - case AVAuthorizationStatusDenied: // The user has specifically denied this app from using the camera - case AVAuthorizationStatusRestricted: // Parental controls are blocking the camera - cameraAllowed = NO; - break; - default: - break; - } - return (cameraHasUsageDescription && cameraAllowed); + return cameraHasUsageDescription; +} + ++ (NSError *)stp_cardScanningError { + NSDictionary *userInfo = @{ + NSLocalizedDescriptionKey: STPLocalizedString(@"To scan your card, you'll need to allow access to your camera in Settings.", @"Error when the user hasn't allowed the current app to access the camera when scanning a payment card. 'Settings' is the localized name of the iOS Settings app."), + STPErrorMessageKey: @"The camera couldn't be used." + }; + return [[NSError alloc] initWithDomain:STPCardScannerErrorDomain code:STPCardScannerErrorCameraNotAvailable userInfo:userInfo]; } - (instancetype)initWithDelegate:(id)delegate { @@ -123,8 +125,12 @@ - (void)start { } - (void)stop { + [self stopWithError:nil]; +} + +- (void)stopWithError:(nullable NSError *)error { if (self.isScanning) { - [self finishWithParams:nil]; + [self finishWithParams:nil error:error]; } } @@ -138,8 +144,7 @@ - (void)setupCamera { return; } if (error) { - NSLog(@"Text recognition error."); - [strongSelf stop]; + [strongSelf stopWithError:[STPCardScanner stp_cardScanningError]]; return; } [strongSelf processVNRequest:request]; @@ -154,15 +159,14 @@ - (void)setupCamera { NSError *deviceInputError; AVCaptureDeviceInput *deviceInput = [[AVCaptureDeviceInput alloc] initWithDevice:captureDevice error:&deviceInputError]; if (deviceInputError) { - NSLog(@"Failed to create camera device input."); - [self stop]; + [self stopWithError:[STPCardScanner stp_cardScanningError]]; return; } if ([self.captureSession canAddInput:deviceInput]) { [self.captureSession addInput:deviceInput]; } else { - [self stop]; + [self stopWithError:[STPCardScanner stp_cardScanningError]]; return; } @@ -170,12 +174,14 @@ - (void)setupCamera { self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init]; self.videoDataOutput.alwaysDiscardsLateVideoFrames = YES; [self.videoDataOutput setSampleBufferDelegate:self queue:self.videoDataOutputQueue]; + + // This is the recommended pixel buffer format for Vision: [self.videoDataOutput setVideoSettings:@{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)}]; + if ([self.captureSession canAddOutput:self.videoDataOutput]) { [self.captureSession addOutput:self.videoDataOutput]; } else { - NSLog(@"Failed to connect our output to the video capture session."); - [self stop]; + [self stopWithError:[STPCardScanner stp_cardScanningError]]; return; } @@ -186,10 +192,7 @@ - (void)setupCamera { NSError *lockError; [self.captureDevice lockForConfiguration:&lockError]; - if (lockError) { - NSLog(@"Failed to lock the camera: Our card scanning session won't be zoomed in."); - } else { - self.captureDevice.videoZoomFactor = 2; + if (lockError == nil) { self.captureDevice.autoFocusRangeRestriction = AVCaptureAutoFocusRangeRestrictionNear; } } @@ -208,11 +211,8 @@ - (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleB self.textRequest.usesLanguageCorrection = NO; self.textRequest.regionOfInterest = self.regionOfInterest; VNImageRequestHandler *handler = [[VNImageRequestHandler alloc] initWithCVPixelBuffer:pixelBuffer orientation:self.textOrientation options:@{}]; - NSError *requestError; + __unused NSError *requestError; [handler performRequests:@[self.textRequest] error:&requestError]; - if (requestError) { - NSLog(@"OCR failed."); - } } - (void)processVNRequest:(VNRequest * _Nonnull)request { @@ -350,11 +350,11 @@ - (void)finishIfReady { params.expMonth = @([[topExpiration substringToIndex:2] integerValue]); params.expYear = @([[topExpiration substringFromIndex:2] integerValue]); } - [self finishWithParams:params]; + [self finishWithParams:params error:nil]; } } -- (void)finishWithParams:(STPPaymentMethodCardParams *)params { +- (void)finishWithParams:(STPPaymentMethodCardParams *)params error:(NSError *)error { NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:self.startTime]; self.isScanning = NO; [self.captureDevice unlockForConfiguration]; @@ -368,14 +368,14 @@ - (void)finishWithParams:(STPPaymentMethodCardParams *)params { } self.cameraView.captureSession = nil; - [self.delegate cardScanner:self didFinishWithCardParams:params]; + [self.delegate cardScanner:self didFinishWithCardParams:params error:error]; }); } #pragma mark Orientation - (void)setDeviceOrientation:(UIDeviceOrientation)newDeviceOrientation { - self._deviceOrientation = newDeviceOrientation; + self._stp_deviceOrientation = newDeviceOrientation; // This is an optimization for portrait mode: The card will be centered in the screen, // so we can ignore the top and bottom. We'll use the whole frame in landscape. @@ -412,7 +412,7 @@ - (void)setDeviceOrientation:(UIDeviceOrientation)newDeviceOrientation { } - (UIDeviceOrientation)deviceOrientation { - return self._deviceOrientation; + return self._stp_deviceOrientation; } @end diff --git a/Stripe/STPCardScannerTableViewCell.m b/Stripe/STPCardScannerTableViewCell.m index c5c3543ae1a..a14eda84661 100644 --- a/Stripe/STPCardScannerTableViewCell.m +++ b/Stripe/STPCardScannerTableViewCell.m @@ -20,7 +20,7 @@ @interface STPCardScannerTableViewCell() @implementation STPCardScannerTableViewCell -static const CGFloat cardSizeRatio = 2.125f/3.370f; +static const CGFloat cardSizeRatio = 2.125f/3.370f; // ID-1 card size (in inches) - (instancetype)init { self = [super init];