Skip to content

Commit

Permalink
Update based on review and IR feedback
Browse files Browse the repository at this point in the history
  • Loading branch information
davidme-stripe committed Aug 28, 2020
1 parent 50da720 commit ebaaf44
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 37 deletions.
3 changes: 3 additions & 0 deletions Stripe/Resources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down
14 changes: 12 additions & 2 deletions Stripe/STPAddCardViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}

Expand Down Expand Up @@ -600,9 +604,14 @@ - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIVi
}
}

- (void)cardScanner:(__unused STPCardScanner *)scanner didFinishWithCardParams:(nullable STPPaymentMethodCardParams *)cardParams API_AVAILABLE(ios(13)){
- (void)cardScanner:(__unused STPCardScanner *)scanner didFinishWithCardParams:(nullable STPPaymentMethodCardParams *)cardParams error:(nullable NSError *)error API_AVAILABLE(ios(13)){
if (error) {
[self handleError:error];
}

static NSTimeInterval const kSTPCardScanAnimationTime = 0.04;
if (cardParams) {
self.view.userInteractionEnabled = NO;
self.paymentCell.paymentField.inputView = [[UIView alloc] init];
__block NSUInteger i = 0;
self.scannerCompleteAnimationTimer = [NSTimer scheduledTimerWithTimeInterval:kSTPCardScanAnimationTime repeats:YES block:^(NSTimer * _Nonnull timer) {
Expand All @@ -622,6 +631,7 @@ - (void)cardScanner:(__unused STPCardScanner *)scanner didFinishWithCardParams:(
[self.paymentCell.paymentField resignFirstResponder];
[self.paymentCell.paymentField becomeFirstResponder];
[timer invalidate];
self.view.userInteractionEnabled = YES;
}
}];
} else {
Expand Down
11 changes: 10 additions & 1 deletion Stripe/STPCardScanner.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,18 @@ NS_ASSUME_NONNULL_BEGIN

@class STPCardScanner, STPPaymentMethodCardParams;

extern NSString *const STPCardScannerErrorDomain;

typedef NS_ENUM(NSInteger, STPCardScannerError) {
/**
Camera not available.
*/
STPCardScannerErrorCameraNotAvailable,
};

API_AVAILABLE(ios(13.0))
@protocol STPCardScannerDelegate <NSObject>
- (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))
Expand Down
66 changes: 33 additions & 33 deletions Stripe/STPCardScanner.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 () <AVCaptureVideoDataOutputSampleBufferDelegate>
@property (nonatomic, weak) id<STPCardScannerDelegate>delegate;
@property (nonatomic, strong) AVCaptureDevice *captureDevice;
Expand All @@ -39,7 +43,7 @@ @interface STPCardScanner () <AVCaptureVideoDataOutputSampleBufferDelegate>
@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;

Expand Down Expand Up @@ -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<STPCardScannerDelegate>)delegate {
Expand Down Expand Up @@ -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];
}
}

Expand All @@ -138,8 +144,7 @@ - (void)setupCamera {
return;
}
if (error) {
NSLog(@"Text recognition error.");
[strongSelf stop];
[strongSelf stopWithError:[STPCardScanner stp_cardScanningError]];
return;
}
[strongSelf processVNRequest:request];
Expand All @@ -154,28 +159,29 @@ - (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;
}

self.videoDataOutputQueue = dispatch_queue_create("com.stripe.CardScanning.VideoDataOutputQueue", nil);
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;
}

Expand All @@ -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;
}
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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];
Expand All @@ -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.
Expand Down Expand Up @@ -412,7 +412,7 @@ - (void)setDeviceOrientation:(UIDeviceOrientation)newDeviceOrientation {
}

- (UIDeviceOrientation)deviceOrientation {
return self._deviceOrientation;
return self._stp_deviceOrientation;
}

@end
2 changes: 1 addition & 1 deletion Stripe/STPCardScannerTableViewCell.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down

0 comments on commit ebaaf44

Please sign in to comment.