// // STPPaymentCardTextField.m // Stripe // // Created by Jack Flintermann on 7/16/15. // Copyright (c) 2015 Stripe, Inc. All rights reserved. // #import //#import "Stripe.h" #import "STPPaymentCardTextField.h" #import "STPPaymentCardTextFieldViewModel.h" #import "STPFormTextField.h" #import "STPImageLibrary.h" #import "STPWeakStrongMacros.h" #define FAUXPAS_IGNORED_IN_METHOD(...) @interface STPPaymentCardTextField() @property(nonatomic, readwrite, strong)STPFormTextField *sizingField; @property(nonatomic, readwrite, weak)UIImageView *brandImageView; @property(nonatomic, readwrite, weak)UIView *fieldsView; @property(nonatomic, readwrite, weak)STPFormTextField *numberField; @property(nonatomic, readwrite, weak)STPFormTextField *expirationField; @property(nonatomic, readwrite, weak)STPFormTextField *cvcField; @property(nonatomic, readwrite, strong)STPPaymentCardTextFieldViewModel *viewModel; @property(nonatomic, readonly, weak)UITextField *currentFirstResponderField; @property(nonatomic, assign)BOOL numberFieldShrunk; @property(nonatomic, readwrite, strong)STPCardParams *internalCardParams; @end @implementation STPPaymentCardTextField @synthesize font = _font; @synthesize textColor = _textColor; @synthesize textErrorColor = _textErrorColor; @synthesize placeholderColor = _placeholderColor; @synthesize borderColor = _borderColor; @synthesize borderWidth = _borderWidth; @synthesize cornerRadius = _cornerRadius; @dynamic enabled; CGFloat const STPPaymentCardTextFieldDefaultPadding = 13; #if CGFLOAT_IS_DOUBLE #define stp_roundCGFloat(x) round(x) #else #define stp_roundCGFloat(x) roundf(x) #endif #pragma mark initializers - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commonInit]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self commonInit]; } return self; } - (void)commonInit { // We're using ivars here because UIAppearance tracks when setters are // called, and won't override properties that have already been customized _borderColor = [self.class placeholderGrayColor]; _cornerRadius = 5.0f; _borderWidth = 1.0f; self.layer.borderColor = [[_borderColor copy] CGColor]; self.layer.cornerRadius = _cornerRadius; self.layer.borderWidth = _borderWidth; self.clipsToBounds = YES; _internalCardParams = [STPCardParams new]; _viewModel = [STPPaymentCardTextFieldViewModel new]; _sizingField = [self buildTextField]; _sizingField.formDelegate = nil; UIImageView *brandImageView = [[UIImageView alloc] initWithImage:self.brandImage]; brandImageView.contentMode = UIViewContentModeCenter; brandImageView.backgroundColor = [UIColor clearColor]; if ([brandImageView respondsToSelector:@selector(setTintColor:)]) { brandImageView.tintColor = self.placeholderColor; } self.brandImageView = brandImageView; STPFormTextField *numberField = [self buildTextField]; numberField.autoFormattingBehavior = STPFormTextFieldAutoFormattingBehaviorCardNumbers; numberField.tag = STPCardFieldTypeNumber; numberField.accessibilityLabel = NSLocalizedString(@"card number", @"accessibility label for text field"); self.numberField = numberField; self.numberPlaceholder = [self.viewModel defaultPlaceholder]; STPFormTextField *expirationField = [self buildTextField]; expirationField.autoFormattingBehavior = STPFormTextFieldAutoFormattingBehaviorExpiration; expirationField.tag = STPCardFieldTypeExpiration; expirationField.alpha = 0; expirationField.accessibilityLabel = NSLocalizedString(@"expiration date", @"accessibility label for text field"); self.expirationField = expirationField; self.expirationPlaceholder = @"MM/YY"; STPFormTextField *cvcField = [self buildTextField]; cvcField.tag = STPCardFieldTypeCVC; cvcField.alpha = 0; self.cvcField = cvcField; self.cvcPlaceholder = @"CVC"; self.cvcField.accessibilityLabel = self.cvcPlaceholder; UIView *fieldsView = [[UIView alloc] init]; fieldsView.clipsToBounds = YES; fieldsView.backgroundColor = [UIColor clearColor]; self.fieldsView = fieldsView; [self addSubview:self.fieldsView]; [self.fieldsView addSubview:cvcField]; [self.fieldsView addSubview:expirationField]; [self.fieldsView addSubview:numberField]; [self addSubview:brandImageView]; } - (STPPaymentCardTextFieldViewModel *)viewModel { if (_viewModel == nil) { _viewModel = [STPPaymentCardTextFieldViewModel new]; } return _viewModel; } #pragma mark appearance properties + (UIColor *)placeholderGrayColor { return [UIColor lightGrayColor]; } - (void)setBackgroundColor:(UIColor *)backgroundColor { [super setBackgroundColor:[backgroundColor copy]]; self.numberField.backgroundColor = self.backgroundColor; } - (UIColor *)backgroundColor { return [super backgroundColor] ?: [UIColor whiteColor]; } - (void)setFont:(UIFont *)font { _font = [font copy]; for (UITextField *field in [self allFields]) { field.font = _font; } self.sizingField.font = _font; [self setNeedsLayout]; } - (UIFont *)font { return _font ?: [UIFont systemFontOfSize:18]; } - (void)setTextColor:(UIColor *)textColor { _textColor = [textColor copy]; for (STPFormTextField *field in [self allFields]) { field.defaultColor = _textColor; } } - (void)setContentVerticalAlignment:(UIControlContentVerticalAlignment)contentVerticalAlignment { [super setContentVerticalAlignment:contentVerticalAlignment]; for (UITextField *field in [self allFields]) { field.contentVerticalAlignment = contentVerticalAlignment; } switch (contentVerticalAlignment) { case UIControlContentVerticalAlignmentCenter: self.brandImageView.contentMode = UIViewContentModeCenter; break; case UIControlContentVerticalAlignmentBottom: self.brandImageView.contentMode = UIViewContentModeBottom; break; case UIControlContentVerticalAlignmentFill: self.brandImageView.contentMode = UIViewContentModeTop; break; case UIControlContentVerticalAlignmentTop: self.brandImageView.contentMode = UIViewContentModeTop; break; } } - (UIColor *)textColor { return _textColor ?: [UIColor blackColor]; } - (void)setTextErrorColor:(UIColor *)textErrorColor { _textErrorColor = [textErrorColor copy]; for (STPFormTextField *field in [self allFields]) { field.errorColor = _textErrorColor; } } - (UIColor *)textErrorColor { return _textErrorColor ?: [UIColor redColor]; } - (void)setPlaceholderColor:(UIColor *)placeholderColor { _placeholderColor = [placeholderColor copy]; if ([self.brandImageView respondsToSelector:@selector(setTintColor:)]) { self.brandImageView.tintColor = placeholderColor; } for (STPFormTextField *field in [self allFields]) { field.placeholderColor = _placeholderColor; } } - (UIColor *)placeholderColor { return _placeholderColor ?: [self.class placeholderGrayColor]; } - (void)setNumberPlaceholder:(NSString * __nullable)numberPlaceholder { _numberPlaceholder = [numberPlaceholder copy]; self.numberField.placeholder = _numberPlaceholder; } - (void)setExpirationPlaceholder:(NSString * __nullable)expirationPlaceholder { _expirationPlaceholder = [expirationPlaceholder copy]; self.expirationField.placeholder = _expirationPlaceholder; } - (void)setCvcPlaceholder:(NSString * __nullable)cvcPlaceholder { _cvcPlaceholder = [cvcPlaceholder copy]; self.cvcField.placeholder = _cvcPlaceholder; } - (void)setCursorColor:(UIColor *)cursorColor { self.tintColor = cursorColor; } - (UIColor *)cursorColor { return self.tintColor; } - (void)setBorderColor:(UIColor * __nullable)borderColor { _borderColor = borderColor; if (borderColor) { self.layer.borderColor = [[borderColor copy] CGColor]; } else { self.layer.borderColor = [[UIColor clearColor] CGColor]; } } - (UIColor * __nullable)borderColor { return _borderColor; } - (void)setCornerRadius:(CGFloat)cornerRadius { _cornerRadius = cornerRadius; self.layer.cornerRadius = cornerRadius; } - (CGFloat)cornerRadius { return _cornerRadius; } - (void)setBorderWidth:(CGFloat)borderWidth { _borderWidth = borderWidth; self.layer.borderWidth = borderWidth; } - (CGFloat)borderWidth { return _borderWidth; } - (void)setKeyboardAppearance:(UIKeyboardAppearance)keyboardAppearance { _keyboardAppearance = keyboardAppearance; for (STPFormTextField *field in [self allFields]) { field.keyboardAppearance = keyboardAppearance; } } - (void)setInputAccessoryView:(UIView *)inputAccessoryView { _inputAccessoryView = inputAccessoryView; for (STPFormTextField *field in [self allFields]) { field.inputAccessoryView = inputAccessoryView; } } #pragma mark UIControl - (void)setEnabled:(BOOL)enabled { [super setEnabled:enabled]; for (STPFormTextField *textField in [self allFields]) { textField.enabled = enabled; }; } #pragma mark UIResponder & related methods - (BOOL)isFirstResponder { return [self.currentFirstResponderField isFirstResponder]; } - (BOOL)canBecomeFirstResponder { return [[self nextFirstResponderField] canBecomeFirstResponder]; } - (BOOL)becomeFirstResponder { return [[self nextFirstResponderField] becomeFirstResponder]; } - (STPFormTextField *)nextFirstResponderField { if ([self.viewModel validationStateForField:STPCardFieldTypeNumber] != STPCardValidationStateValid) { return self.numberField; } else if ([self.viewModel validationStateForField:STPCardFieldTypeExpiration] != STPCardValidationStateValid) { return self.expirationField; } else { return self.cvcField; } } - (STPFormTextField *)currentFirstResponderField { for (STPFormTextField *textField in [self allFields]) { if ([textField isFirstResponder]) { return textField; } } return nil; } - (BOOL)canResignFirstResponder { return [self.currentFirstResponderField canResignFirstResponder]; } - (BOOL)resignFirstResponder { [super resignFirstResponder]; BOOL success = [self.currentFirstResponderField resignFirstResponder]; [self setNumberFieldShrunk:[self shouldShrinkNumberField] animated:YES completion:nil]; [self updateImageForFieldType:STPCardFieldTypeNumber]; return success; } - (STPFormTextField *)previousField { if (self.currentFirstResponderField == self.cvcField) { return self.expirationField; } else if (self.currentFirstResponderField == self.expirationField) { return self.numberField; } return nil; } #pragma mark public convenience methods - (void)clear { for (STPFormTextField *field in [self allFields]) { field.text = @""; } self.viewModel = [STPPaymentCardTextFieldViewModel new]; [self onChange]; [self updateImageForFieldType:STPCardFieldTypeNumber]; WEAK(self); [self setNumberFieldShrunk:NO animated:YES completion:^(__unused BOOL completed){ STRONG(self); if ([self isFirstResponder]) { [[self numberField] becomeFirstResponder]; } }]; } - (BOOL)isValid { return [self.viewModel isValid]; } - (BOOL)valid { return self.isValid; } #pragma mark readonly variables - (NSString *)cardNumber { return self.viewModel.cardNumber; } - (NSUInteger)expirationMonth { return [self.viewModel.expirationMonth integerValue]; } - (NSUInteger)expirationYear { return [self.viewModel.expirationYear integerValue]; } - (NSString *)formattedExpirationMonth { return self.viewModel.expirationMonth; } - (NSString *)formattedExpirationYear { return self.viewModel.expirationYear; } - (NSString *)cvc { return self.viewModel.cvc; } - (STPCardParams *)cardParams { self.internalCardParams.number = self.cardNumber; self.internalCardParams.expMonth = self.expirationMonth; self.internalCardParams.expYear = self.expirationYear; self.internalCardParams.cvc = self.cvc; return self.internalCardParams; } - (void)setCardParams:(STPCardParams *)cardParams { self.internalCardParams = cardParams; [self setText:cardParams.number inField:STPCardFieldTypeNumber]; BOOL expirationPresent = cardParams.expMonth && cardParams.expYear; if (expirationPresent) { NSString *text = [NSString stringWithFormat:@"%02lu%02lu", (unsigned long)cardParams.expMonth, (unsigned long)cardParams.expYear%100]; [self setText:text inField:STPCardFieldTypeExpiration]; } else { [self setText:@"" inField:STPCardFieldTypeExpiration]; } [self setText:cardParams.cvc inField:STPCardFieldTypeCVC]; BOOL shrinkNumberField = [self shouldShrinkNumberField]; [self setNumberFieldShrunk:shrinkNumberField animated:NO completion:nil]; if ([self isFirstResponder]) { [[self nextFirstResponderField] becomeFirstResponder]; } // update the card image, falling back to the number field image if not editing if ([self.expirationField isFirstResponder]) { [self updateImageForFieldType:STPCardFieldTypeExpiration]; } else if ([self.cvcField isFirstResponder]) { [self updateImageForFieldType:STPCardFieldTypeCVC]; } else { [self updateImageForFieldType:STPCardFieldTypeNumber]; } } - (STPCardParams *)card { if (!self.isValid) { return nil; } return self.cardParams; } - (void)setCard:(STPCardParams *)card { [self setCardParams:card]; } - (void)setText:(NSString *)text inField:(STPCardFieldType)field { NSString *nonNilText = text ?: @""; STPFormTextField *textField = nil; switch (field) { case STPCardFieldTypeNumber: textField = self.numberField; break; case STPCardFieldTypeExpiration: textField = self.expirationField; break; case STPCardFieldTypeCVC: textField = self.cvcField; break; } textField.text = nonNilText; } - (CGSize)intrinsicContentSize { CGSize imageSize = self.brandImage.size; self.sizingField.text = self.viewModel.defaultPlaceholder; CGFloat textHeight = [self.sizingField measureTextSize].height; CGFloat imageHeight = imageSize.height + (STPPaymentCardTextFieldDefaultPadding); CGFloat height = stp_roundCGFloat((MAX(MAX(imageHeight, textHeight), 44))); CGFloat width = stp_roundCGFloat([self widthForCardNumber:self.viewModel.defaultPlaceholder] + imageSize.width + (STPPaymentCardTextFieldDefaultPadding * 3)); return CGSizeMake(width, height); } - (CGRect)brandImageRectForBounds:(CGRect)bounds { return CGRectMake(STPPaymentCardTextFieldDefaultPadding, 0, self.brandImageView.image.size.width, bounds.size.height - 1); } - (CGRect)fieldsRectForBounds:(CGRect)bounds { CGRect brandImageRect = [self brandImageRectForBounds:bounds]; return CGRectMake(CGRectGetMaxX(brandImageRect), 0, CGRectGetWidth(bounds) - CGRectGetMaxX(brandImageRect), CGRectGetHeight(bounds)); } - (CGRect)numberFieldRectForBounds:(CGRect)bounds { CGFloat placeholderWidth = [self widthForCardNumber:self.numberField.placeholder] - 4; CGFloat numberWidth = [self widthForCardNumber:self.viewModel.defaultPlaceholder] - 4; CGFloat numberFieldWidth = MAX(placeholderWidth, numberWidth); CGFloat nonFragmentWidth = [self widthForCardNumber:[self.viewModel numberWithoutLastDigits]] - 12; CGFloat numberFieldX = self.numberFieldShrunk ? STPPaymentCardTextFieldDefaultPadding - nonFragmentWidth : 8; return CGRectMake(numberFieldX, 0, numberFieldWidth, CGRectGetHeight(bounds)); } - (CGRect)cvcFieldRectForBounds:(CGRect)bounds { CGRect fieldsRect = [self fieldsRectForBounds:bounds]; CGFloat cvcWidth = MAX([self widthForText:self.cvcField.placeholder], [self widthForText:@"8888"]); CGFloat cvcX = self.numberFieldShrunk ? CGRectGetWidth(fieldsRect) - cvcWidth - STPPaymentCardTextFieldDefaultPadding / 2 : CGRectGetWidth(fieldsRect); return CGRectMake(cvcX, 0, cvcWidth, CGRectGetHeight(bounds)); } - (CGRect)expirationFieldRectForBounds:(CGRect)bounds { CGRect numberFieldRect = [self numberFieldRectForBounds:bounds]; CGRect cvcRect = [self cvcFieldRectForBounds:bounds]; CGFloat expirationWidth = MAX([self widthForText:self.expirationField.placeholder], [self widthForText:@"88/88"]); CGFloat expirationX = (CGRectGetMaxX(numberFieldRect) + CGRectGetMinX(cvcRect) - expirationWidth) / 2; return CGRectMake(expirationX, 0, expirationWidth, CGRectGetHeight(bounds)); } - (void)layoutSubviews { [super layoutSubviews]; CGRect bounds = self.bounds; self.brandImageView.frame = [self brandImageRectForBounds:bounds]; self.fieldsView.frame = [self fieldsRectForBounds:bounds]; self.numberField.frame = [self numberFieldRectForBounds:bounds]; self.cvcField.frame = [self cvcFieldRectForBounds:bounds]; self.expirationField.frame = [self expirationFieldRectForBounds:bounds]; } #pragma mark - private helper methods - (STPFormTextField *)buildTextField { STPFormTextField *textField = [[STPFormTextField alloc] initWithFrame:CGRectZero]; textField.backgroundColor = [UIColor clearColor]; textField.keyboardType = UIKeyboardTypePhonePad; textField.font = self.font; textField.defaultColor = self.textColor; textField.errorColor = self.textErrorColor; textField.placeholderColor = self.placeholderColor; textField.formDelegate = self; textField.validText = true; return textField; } - (NSArray *)allFields { NSMutableArray *mutable = [NSMutableArray array]; if (self.numberField) { [mutable addObject:self.numberField]; } if (self.expirationField) { [mutable addObject:self.expirationField]; } if (self.cvcField) { [mutable addObject:self.cvcField]; } return [mutable copy]; } typedef void (^STPNumberShrunkCompletionBlock)(BOOL completed); - (void)setNumberFieldShrunk:(BOOL)shrunk animated:(BOOL)animated completion:(STPNumberShrunkCompletionBlock)completion { if (_numberFieldShrunk == shrunk) { if (completion) { completion(YES); } return; } _numberFieldShrunk = shrunk; void (^animations)() = ^void() { for (UIView *view in @[self.expirationField, self.cvcField]) { view.alpha = 1.0f * shrunk; } [self layoutSubviews]; }; FAUXPAS_IGNORED_IN_METHOD(APIAvailability); NSTimeInterval duration = animated * 0.3; if ([UIView respondsToSelector:@selector(animateWithDuration:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)]) { [UIView animateWithDuration:duration delay:0 usingSpringWithDamping:0.85f initialSpringVelocity:0 options:0 animations:animations completion:completion]; } else { [UIView animateWithDuration:duration animations:animations completion:completion]; } } - (BOOL)shouldShrinkNumberField { return [self.viewModel validationStateForField:STPCardFieldTypeNumber] == STPCardValidationStateValid; } - (CGFloat)widthForText:(NSString *)text { self.sizingField.autoFormattingBehavior = STPFormTextFieldAutoFormattingBehaviorNone; [self.sizingField setText:text]; return [self.sizingField measureTextSize].width + 8; } - (CGFloat)widthForCardNumber:(NSString *)cardNumber { self.sizingField.autoFormattingBehavior = STPFormTextFieldAutoFormattingBehaviorCardNumbers; [self.sizingField setText:cardNumber]; return [self.sizingField measureTextSize].width + 20; } #pragma mark STPFormTextFieldDelegate - (void)formTextFieldDidBackspaceOnEmpty:(__unused STPFormTextField *)formTextField { STPFormTextField *previous = [self previousField]; [previous becomeFirstResponder]; [previous deleteBackward]; } - (NSAttributedString *)formTextField:(STPFormTextField *)formTextField modifyIncomingTextChange:(NSAttributedString *)input { STPCardFieldType fieldType = formTextField.tag; switch (fieldType) { case STPCardFieldTypeNumber: self.viewModel.cardNumber = input.string; break; case STPCardFieldTypeExpiration: { self.viewModel.rawExpiration = input.string; break; } case STPCardFieldTypeCVC: self.viewModel.cvc = input.string; break; } switch (fieldType) { case STPCardFieldTypeNumber: return [[NSAttributedString alloc] initWithString:self.viewModel.cardNumber attributes:self.numberField.defaultTextAttributes]; case STPCardFieldTypeExpiration: return [[NSAttributedString alloc] initWithString:self.viewModel.rawExpiration attributes:self.expirationField.defaultTextAttributes]; case STPCardFieldTypeCVC: return [[NSAttributedString alloc] initWithString:self.viewModel.cvc attributes:self.cvcField.defaultTextAttributes]; } } - (void)formTextFieldTextDidChange:(STPFormTextField *)formTextField { STPCardFieldType fieldType = formTextField.tag; if (fieldType == STPCardFieldTypeNumber) { [self updateImageForFieldType:fieldType]; } STPCardValidationState state = [self.viewModel validationStateForField:fieldType]; formTextField.validText = YES; switch (state) { case STPCardValidationStateInvalid: formTextField.validText = NO; break; case STPCardValidationStateIncomplete: break; case STPCardValidationStateValid: { [[self nextFirstResponderField] becomeFirstResponder]; break; } } [self onChange]; } - (void)textFieldDidBeginEditing:(UITextField *)textField { switch ((STPCardFieldType)textField.tag) { case STPCardFieldTypeNumber: [self setNumberFieldShrunk:NO animated:YES completion:nil]; if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidBeginEditingNumber:)]) { [self.delegate paymentCardTextFieldDidBeginEditingNumber:self]; } break; case STPCardFieldTypeCVC: [self setNumberFieldShrunk:YES animated:YES completion:nil]; if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidBeginEditingCVC:)]) { [self.delegate paymentCardTextFieldDidBeginEditingCVC:self]; } break; case STPCardFieldTypeExpiration: [self setNumberFieldShrunk:YES animated:YES completion:nil]; if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidBeginEditingExpiration:)]) { [self.delegate paymentCardTextFieldDidBeginEditingExpiration:self]; } break; } [self updateImageForFieldType:textField.tag]; } - (BOOL)textFieldShouldEndEditing:(__unused UITextField *)textField { [self updateImageForFieldType:STPCardFieldTypeNumber]; return YES; } - (void)textFieldDidEndEditing:(UITextField *)textField { switch ((STPCardFieldType)textField.tag) { case STPCardFieldTypeNumber: if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidEndEditingNumber:)]) { [self.delegate paymentCardTextFieldDidEndEditingNumber:self]; } break; case STPCardFieldTypeCVC: if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidEndEditingCVC:)]) { [self.delegate paymentCardTextFieldDidEndEditingCVC:self]; } break; case STPCardFieldTypeExpiration: if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidEndEditingExpiration:)]) { [self.delegate paymentCardTextFieldDidEndEditingExpiration:self]; } break; } } - (UIImage *)brandImage { if (self.currentFirstResponderField) { return [self brandImageForFieldType:self.currentFirstResponderField.tag]; } else { return [self brandImageForFieldType:STPCardFieldTypeNumber]; } } + (UIImage *)cvcImageForCardBrand:(STPCardBrand)cardBrand { return [STPImageLibrary cvcImageForCardBrand:cardBrand]; } + (UIImage *)brandImageForCardBrand:(STPCardBrand)cardBrand { return [STPImageLibrary brandImageForCardBrand:cardBrand]; } - (UIImage *)brandImageForFieldType:(STPCardFieldType)fieldType { if (fieldType == STPCardFieldTypeCVC) { return [self.class cvcImageForCardBrand:self.viewModel.brand]; } return [self.class brandImageForCardBrand:self.viewModel.brand]; } - (void)updateImageForFieldType:(STPCardFieldType)fieldType { UIImage *image = [self brandImageForFieldType:fieldType]; if (image != self.brandImageView.image) { self.brandImageView.image = image; CATransition *transition = [CATransition animation]; transition.duration = 0.2f; transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; transition.type = kCATransitionFade; [self.brandImageView.layer addAnimation:transition forKey:nil]; [self setNeedsLayout]; } } - (void)onChange { if ([self.delegate respondsToSelector:@selector(paymentCardTextFieldDidChange:)]) { [self.delegate paymentCardTextFieldDidChange:self]; } [self sendActionsForControlEvents:UIControlEventValueChanged]; } #pragma mark UIKeyInput - (BOOL)hasText { return self.numberField.hasText || self.expirationField.hasText || self.cvcField.hasText; } - (void)insertText:(NSString *)text { [self.currentFirstResponderField insertText:text]; } - (void)deleteBackward { [self.currentFirstResponderField deleteBackward]; } @end #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" @implementation PTKCard @end @interface PTKView() @property(nonatomic, weak)idinternalDelegate; @end @implementation PTKView @dynamic delegate, card; - (void)setDelegate:(id __nullable)delegate { self.internalDelegate = delegate; } - (id __nullable)delegate { return self.internalDelegate; } - (void)onChange { [super onChange]; [self.internalDelegate paymentView:self withCard:[self card] isValid:self.isValid]; } - (PTKCard * __nonnull)card { PTKCard *card = [[PTKCard alloc] init]; card.number = self.cardNumber; card.expMonth = self.expirationMonth; card.expYear = self.expirationYear; card.cvc = self.cvc; return card; } @end #pragma clang diagnostic pop