BTUIKCardNumberFormField.m 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. #import "BTUIKCardNumberFormField.h"
  2. #import "BTUIKPaymentOptionCardView.h"
  3. #import "BTUIKLocalizedString.h"
  4. #import "BTUIKUtil.h"
  5. #import "BTUIKTextField.h"
  6. #import "BTUIKViewUtil.h"
  7. #import "BTUIKInputAccessoryToolbar.h"
  8. #import "BTUIKAppearance.h"
  9. #define TEMP_KERNING 8.0
  10. @interface BTUIKCardNumberFormField ()
  11. @property (nonatomic, strong) BTUIKPaymentOptionCardView *hint;
  12. @property (nonatomic, strong) UIButton *validateButton;
  13. @property (nonatomic, strong) UIActivityIndicatorView *loadingView;
  14. @end
  15. @implementation BTUIKCardNumberFormField
  16. @synthesize number = _number;
  17. - (instancetype)initWithFrame:(CGRect)frame {
  18. self = [super initWithFrame:frame];
  19. if (self) {
  20. self.state = BTUIKCardNumberFormFieldStateDefault;
  21. self.textField.accessibilityLabel = BTUIKLocalizedString(CARD_NUMBER_PLACEHOLDER);
  22. self.textField.placeholder = BTUIKLocalizedString(CARD_NUMBER_PLACEHOLDER);
  23. self.formLabel.text = @"";
  24. self.textField.keyboardType = UIKeyboardTypeNumberPad;
  25. self.hint = [BTUIKPaymentOptionCardView new];
  26. self.hint.paymentOptionType = BTUIKPaymentOptionTypeUnknown;
  27. self.hint.translatesAutoresizingMaskIntoConstraints = NO;
  28. [self.hint addConstraint:[NSLayoutConstraint constraintWithItem:self.hint attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:[BTUIKAppearance smallIconHeight]]];
  29. [self.hint addConstraint:[NSLayoutConstraint constraintWithItem:self.hint attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:[BTUIKAppearance smallIconWidth]]];
  30. self.accessoryView = self.hint;
  31. [self setAccessoryViewHidden:YES animated:NO];
  32. self.validateButton = [UIButton new];
  33. [self.validateButton setTitle:BTUIKLocalizedString(NEXT_ACTION) forState:UIControlStateNormal];
  34. NSAttributedString *normalValidateButtonString = [[NSAttributedString alloc] initWithString:BTUIKLocalizedString(NEXT_ACTION)
  35. attributes:@{NSForegroundColorAttributeName:[BTUIKAppearance sharedInstance].tintColor,
  36. NSFontAttributeName:[[BTUIKAppearance sharedInstance].boldFont fontWithSize:UIFont.labelFontSize]}];
  37. [self.validateButton setAttributedTitle:normalValidateButtonString
  38. forState:UIControlStateNormal];
  39. NSAttributedString *disabledValidateButtonString = [[NSAttributedString alloc] initWithString:BTUIKLocalizedString(NEXT_ACTION)
  40. attributes:@{NSForegroundColorAttributeName:[BTUIKAppearance sharedInstance].disabledColor,
  41. NSFontAttributeName:[[BTUIKAppearance sharedInstance].boldFont fontWithSize:UIFont.labelFontSize]}];
  42. [self.validateButton setAttributedTitle:disabledValidateButtonString forState:UIControlStateDisabled];
  43. [self.validateButton sizeToFit];
  44. [self.validateButton layoutIfNeeded];
  45. [self.validateButton addTarget:self action:@selector(validateButtonPressed) forControlEvents:UIControlEventTouchUpInside];
  46. [self updateValidationButton];
  47. self.loadingView = [UIActivityIndicatorView new];
  48. self.loadingView.activityIndicatorViewStyle = [BTUIKAppearance sharedInstance].activityIndicatorViewStyle;
  49. [self.loadingView sizeToFit];
  50. }
  51. return self;
  52. }
  53. - (void)validateButtonPressed {
  54. if (self.cardNumberDelegate != nil) {
  55. [self.cardNumberDelegate validateButtonPressed:self];
  56. }
  57. }
  58. - (void)updateValidationButton {
  59. self.validateButton.enabled = _number.length > 13;
  60. }
  61. - (BOOL)valid {
  62. return [self.cardType validNumber:self.number];
  63. }
  64. - (BOOL)entryComplete {
  65. return [super entryComplete] && [self.cardType validAndNecessarilyCompleteNumber:self.number];
  66. }
  67. - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
  68. NSUInteger newLength = textField.text.length - range.length + string.length;
  69. NSUInteger maxLength = self.cardType == nil ? [BTUIKCardType maxNumberLength] : self.cardType.maxNumberLength;
  70. if ([self isShowingValidateButton]) {
  71. return YES;
  72. } else {
  73. return newLength <= maxLength;
  74. }
  75. }
  76. - (void)setText:(NSString *)text {
  77. [super setText:text];
  78. [self fieldContentDidChange];
  79. }
  80. - (void)fieldContentDidChange {
  81. _number = [BTUIKUtil stripNonDigits:self.textField.text];
  82. BTUIKCardType *oldCardType = _cardType;
  83. _cardType = [BTUIKCardType cardTypeForNumber:_number];
  84. [self formatCardNumber];
  85. if (self.cardType != oldCardType) {
  86. [self updateCardHint];
  87. }
  88. self.displayAsValid = self.valid || (!self.isValidLength && self.isPotentiallyValid) || self.state == BTUIKCardNumberFormFieldStateValidate;
  89. [self updateValidationButton];
  90. [self updateAppearance];
  91. [self setNeedsDisplay];
  92. [self.delegate formFieldDidChange:self];
  93. }
  94. - (void)formatCardNumber {
  95. if (self.cardType != nil) {
  96. UITextRange *r = self.textField.selectedTextRange;
  97. NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithAttributedString:[self.cardType formatNumber:_number kerning:TEMP_KERNING]];
  98. self.textField.attributedText = text;
  99. self.textField.selectedTextRange = r;
  100. }
  101. }
  102. - (void)textFieldDidBeginEditing:(UITextField *)textField {
  103. self.textField.text = _number;
  104. [super textFieldDidBeginEditing:textField];
  105. self.displayAsValid = self.valid || (!self.isValidLength && self.isPotentiallyValid);
  106. self.formLabel.text = @"";
  107. [UIView transitionWithView:self
  108. duration:0.2
  109. options:UIViewAnimationOptionTransitionCrossDissolve
  110. animations:^{
  111. if ([self isShowingValidateButton]) {
  112. [self setAccessoryViewHidden:NO animated:NO];
  113. } else {
  114. [self setAccessoryViewHidden:YES animated:YES];
  115. }
  116. [self updateConstraints];
  117. [self updateAppearance];
  118. if (self.isPotentiallyValid) {
  119. [self formatCardNumber];
  120. }
  121. } completion:nil];
  122. }
  123. - (void)textFieldDidEndEditing:(UITextField *)textField {
  124. [super textFieldDidEndEditing:textField];
  125. self.displayAsValid = self.number.length == 0 || (![self isValidLength] && self.state == BTUIKCardNumberFormFieldStateValidate) || (_cardType != nil && [_cardType validNumber:_number]);
  126. self.formLabel.text = self.number.length == 0 || (![self isValidLength] && self.state == BTUIKCardNumberFormFieldStateValidate) ? @"" : BTUIKLocalizedString(CARD_NUMBER_PLACEHOLDER);
  127. [UIView animateWithDuration:0.2 animations:^{
  128. if ([self isShowingValidateButton]) {
  129. [self setAccessoryViewHidden:NO animated:NO];
  130. } else {
  131. if (self.number.length == 0) {
  132. [self setAccessoryViewHidden:YES animated:YES];
  133. } else {
  134. [self showCardHintAccessory];
  135. }
  136. }
  137. if (self.number.length > 7 && ([self isValidLength] || self.state != BTUIKCardNumberFormFieldStateValidate)) {
  138. NSString *lastFour = [self.number substringFromIndex: [self.number length] - 4];
  139. self.textField.text = [NSString stringWithFormat:@"•••• %@", lastFour];
  140. }
  141. [self updateConstraints];
  142. [self updateAppearance];
  143. }];
  144. }
  145. - (void)resetFormField {
  146. self.formLabel.text = @"";
  147. self.textField.text = @"";
  148. [self setAccessoryViewHidden:YES animated:NO];
  149. [self updateConstraints];
  150. [self updateAppearance];
  151. }
  152. #pragma mark - Public Methods
  153. - (void)setState:(BTUIKCardNumberFormFieldState)state {
  154. if (state == self.state) {
  155. return;
  156. }
  157. _state = state;
  158. if (self.state == BTUIKCardNumberFormFieldStateDefault) {
  159. self.accessoryView = self.hint;
  160. [self setAccessoryViewHidden:(self.formLabel.text.length <= 0) animated:YES];
  161. } else if (self.state == BTUIKCardNumberFormFieldStateLoading) {
  162. self.accessoryView = self.loadingView;
  163. [self setAccessoryViewHidden:NO animated:YES];
  164. [self.loadingView startAnimating];
  165. } else {
  166. self.accessoryView = self.validateButton;
  167. [self setAccessoryViewHidden:NO animated:YES];
  168. }
  169. }
  170. - (void)setNumber:(NSString *)number {
  171. self.text = number;
  172. _number = self.textField.text;
  173. }
  174. - (void)showCardHintAccessory {
  175. [self setAccessoryViewHidden:NO animated:YES];
  176. }
  177. #pragma mark - Private Helpers
  178. - (BOOL)isShowingValidateButton {
  179. return self.state == BTUIKCardNumberFormFieldStateValidate;
  180. }
  181. - (BOOL)isValidCardType {
  182. return self.cardType != nil || _number.length == 0;
  183. }
  184. - (BOOL)isPotentiallyValid {
  185. return [BTUIKCardType cardTypeForNumber:self.number] != nil;
  186. }
  187. - (BOOL)isValidLength {
  188. return self.cardType != nil && [self.cardType completeNumber:_number];
  189. }
  190. - (void)updateCardHint {
  191. BTUIKPaymentOptionType paymentMethodType = [BTUIKViewUtil paymentMethodTypeForCardType:self.cardType];
  192. self.hint.paymentOptionType = paymentMethodType;
  193. }
  194. @end