diff --git a/README.md b/README.md index c1e0ba5..c8b4f50 100644 --- a/README.md +++ b/README.md @@ -20,26 +20,35 @@ react-native link react-native-cloudpayments ``` # Methods -### isValidCard() -Validate card. +### isValidNumber() +Validate card number. Returns a `Promise` that resolve card status (`Boolean`). __Arguments__ - `cardNumber` - `String` Number of payment card. -- `cardExp` - `String` Expire date of payment card. -- `cardCvv` - `String` CVV code of payment card. __Examples__ ```js import RNCloudPayment from 'react-native-cloudpayments'; -const demoCard = { - number: '5105105105105100', - extDate: '10/18', - cvvCode: '123', -}; +RNCloudPayment.isValidNumber('5105105105105100') + .then(cardStatus => { + console.log(cardStatus); // true + }); +``` + +### isValidExpired() +Validate card expired. +Returns a `Promise` that resolve card status (`Boolean`). + +__Arguments__ +- `cardExp` - `String` Expire date of payment card. + +__Examples__ +```js +import RNCloudPayment from 'react-native-cloudpayments'; -RNCloudPayment.isValidCard(demoCard.number, demoCard.extDate, demoCard.cvvCode) +RNCloudPayment.isValidExpired('11/21') .then(cardStatus => { console.log(cardStatus); // true }); @@ -106,5 +115,24 @@ RNCloudPayment.createCryptogram(demoCard.number, demoCard.extDate, demoCard.cvvC }); ``` +### show3DS() +Show 3ds secure. +Returns a `Promise` that resolve cryptogram (`Object`). + +__Arguments__ +- `url` - `String` Url redirect. +- `transactionId` - `String` Transaction ID. +- `token` - `String` Token. + +__Examples__ +```js +import RNCloudPayment from 'react-native-cloudpayments'; + +RNCloudPayment.show3DS('https://demo.cloudpayments.ru', '1237618734', '....1d3d22r..') + .then(result => { + console.log(result); + }); +``` + # License Licensed under the MIT License. diff --git a/RNCloudPayments.podspec b/RNCloudPayments.podspec new file mode 100644 index 0000000..531eb46 --- /dev/null +++ b/RNCloudPayments.podspec @@ -0,0 +1,14 @@ +Pod::Spec.new do |spec| + spec.name = "RNCloudPayments" + spec.version = "1.0.0" + spec.summary = "React Native library for using CloudPayments SDK" + spec.description = "React Native library for using CloudPayments SDK" + spec.homepage = "https://github.com/kakadu-dev/react-native-cloudpayments" + spec.license = { :type => "MIT" } + spec.author = { "Nikolay Polukhin" => "polu-hin@mail.ru" } + spec.platform = :ios + spec.ios.deployment_target = "9.0" + spec.source = { :git => "https://github.com/kakadu-dev/react-native-cloudpayments.git", :tag => "#{spec.version}" } + spec.source_files = "RNCloudPayments", "ios/*.{h,m}", "ios/Extensions/*.{h,m}", "ios/SDK/NSDataENBase64.{h,m}", "ios/SDWebViewController/*.{h,m}" + spec.dependency "React" + end diff --git a/android/build.gradle b/android/build.gradle index 9796197..0ef7580 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -12,12 +12,12 @@ buildscript { apply plugin: 'com.android.library' android { - compileSdkVersion 27 - buildToolsVersion "27.0.3" + compileSdkVersion 28 + buildToolsVersion = "28.0.3" defaultConfig { minSdkVersion 19 - targetSdkVersion 27 + targetSdkVersion 28 versionCode 1 versionName "1.0" } @@ -32,5 +32,5 @@ repositories { dependencies { implementation "com.facebook.react:react-native:+" - implementation files('src/main/libs/CloudPayments.aar') + implementation 'ru.cloudpayments.android:sdk:1.0.3' } diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index c066142..77529b4 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,9 +1,12 @@ - + + - + android:allowBackup="false" + tools:replace="android:allowBackup"> + + + + \ No newline at end of file diff --git a/android/src/main/java/com/rncloudpayments/CheckoutActivity.java b/android/src/main/java/com/rncloudpayments/CheckoutActivity.java new file mode 100644 index 0000000..b1e1866 --- /dev/null +++ b/android/src/main/java/com/rncloudpayments/CheckoutActivity.java @@ -0,0 +1,66 @@ +package com.rncloudpayments; + +import android.app.Activity; +import android.app.FragmentManager; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.Promise; +import com.facebook.react.bridge.WritableMap; +import ru.cloudpayments.sdk.three_ds.ThreeDSDialogListener; +import ru.cloudpayments.sdk.three_ds.ThreeDsDialogFragment; + +public class CheckoutActivity extends Activity implements ThreeDSDialogListener { + + static String acsUrl; + static String paReq; + static String transactionId; + static Promise promise; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + FragmentManager fm = getFragmentManager(); + + final ThreeDsDialogFragment dialog = ThreeDsDialogFragment.newInstance(acsUrl, transactionId, paReq); + dialog.show(fm, "3DS"); + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + dialog.getView().addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + } + + @Override + public void onViewDetachedFromWindow(View v) { + promise.reject("Cancel"); + + promise = null; + + finish(); + } + }); + } + },50); + + } + + @Override + public void onAuthorizationCompleted(String md, String paRes) { + WritableMap map = Arguments.createMap(); + + map.putString("MD", md); + map.putString("PaRes", paRes); + + promise.resolve(map); + } + + @Override + public void onAuthorizationFailed(String html) { + promise.reject(html); + } +} diff --git a/android/src/main/java/com/rncloudpayments/CloudPayments.java b/android/src/main/java/com/rncloudpayments/CloudPayments.java index 269ca7b..855ebb0 100644 --- a/android/src/main/java/com/rncloudpayments/CloudPayments.java +++ b/android/src/main/java/com/rncloudpayments/CloudPayments.java @@ -1,12 +1,13 @@ package com.rncloudpayments; +import android.content.Intent; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import ru.cloudpayments.sdk.cp_card.CPCard; +import ru.cloudpayments.sdk.three_ds.ThreeDsDialogFragment; -import ru.cloudpayments.cpcard.CPCard; -import ru.cloudpayments.cpcard.CPCardFactory; public class CloudPayments extends ReactContextBaseJavaModule { public CloudPayments(ReactApplicationContext reactContext) { @@ -19,11 +20,10 @@ public String getName() { } @ReactMethod - public void isValidNumber(String cardNumber, String cardExp, String cardCvv, Promise promise) { + public void isValidNumber(String cardNumber, Promise promise) { try { - CPCard card = CPCardFactory.create(cardNumber, cardExp, cardCvv); - - boolean numberStatus = card.isValidNumber(); + String validFormatNumber = cardNumber.replace(" ", ""); + boolean numberStatus = CPCard.isValidNumber(validFormatNumber); promise.resolve(numberStatus); } catch (Exception e) { @@ -31,10 +31,22 @@ public void isValidNumber(String cardNumber, String cardExp, String cardCvv, Pro } } + @ReactMethod + public void isValidExpired(String cardExpired, Promise promise) { + try { + String validFormatExp = cardExpired.replace("/", ""); + boolean expiredStatus = CPCard.isValidExpDate(validFormatExp); + + promise.resolve(expiredStatus); + } catch (Exception e) { + promise.reject(e.getMessage()); + } + } + @ReactMethod public void getType(String cardNumber, String cardExp, String cardCvv, Promise promise) { try { - CPCard card = CPCardFactory.create(cardNumber, cardExp, cardCvv); + CPCard card = new CPCard(cardNumber, cardExp, cardCvv); String cardType = card.getType(); @@ -47,7 +59,10 @@ public void getType(String cardNumber, String cardExp, String cardCvv, Promise p @ReactMethod public void createCryptogram(String cardNumber, String cardExp, String cardCvv, String publicId, Promise promise) { try { - CPCard card = CPCardFactory.create(cardNumber, cardExp, cardCvv); + String validFormatNumber = cardNumber.replace(" ", ""); + String validFormatExp = cardExp.replace("/", ""); + + CPCard card = new CPCard(validFormatNumber, validFormatExp, cardCvv); String cryptoprogram = card.cardCryptogram(publicId); @@ -57,4 +72,22 @@ public void createCryptogram(String cardNumber, String cardExp, String cardCvv, promise.reject(e.getMessage()); } } + + @ReactMethod + public void show3DS(String acsUrl, String paReq, String transactionId, Promise promise) { + try { + Intent intent = new Intent(getReactApplicationContext(), CheckoutActivity.class); + + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + CheckoutActivity.acsUrl = acsUrl; + CheckoutActivity.paReq = paReq; + CheckoutActivity.transactionId = transactionId; + CheckoutActivity.promise = promise; + + getReactApplicationContext().startActivity(intent); + } catch (Exception e) { + promise.reject(e.getMessage()); + } + } } \ No newline at end of file diff --git a/android/src/main/libs/CloudPayments.aar b/android/src/main/libs/CloudPayments.aar deleted file mode 100755 index c0cce8a..0000000 Binary files a/android/src/main/libs/CloudPayments.aar and /dev/null differ diff --git a/android/src/main/res/values/strings.xml b/android/src/main/res/values/strings.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/android/src/main/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/index.js b/index.js index 4311031..92d79dc 100644 --- a/index.js +++ b/index.js @@ -3,14 +3,22 @@ import { NativeModules } from 'react-native'; const RNCloudPaymentsModule = NativeModules.RNCloudPayments; export default class RNCloudPayments { - static async isValidCard(cardNumber, cardExp, cardCvv) { + static async isValidNumber(cardNumber) { try { - return await RNCloudPaymentsModule.isValidNumber(cardNumber, cardExp, cardCvv); + return await RNCloudPaymentsModule.isValidNumber(cardNumber); } catch(error) { return createError(error); } } + static async isValidExpired(cardExp) { + try { + return await RNCloudPaymentsModule.isValidExpired(cardExp); + } catch(error) { + return createError(error); + } + } + static async getType(cardNumber, cardExp, cardCvv) { try { return await RNCloudPaymentsModule.getType(cardNumber, cardExp, cardCvv); @@ -26,6 +34,14 @@ export default class RNCloudPayments { return createError(error); } } + + static async show3DS(acsUrl, paReq, transactionId) { + try { + return await RNCloudPaymentsModule.show3DS(acsUrl, paReq, transactionId); + } catch(error) { + return createError(error); + } + } } class RNCloudPaymentsError extends Error { diff --git a/ios/Extensions/NSString+URLEncoding.h b/ios/Extensions/NSString+URLEncoding.h new file mode 100644 index 0000000..7925bfb --- /dev/null +++ b/ios/Extensions/NSString+URLEncoding.h @@ -0,0 +1,15 @@ +// +// NSString+URLEncoding.h +// +// Created by Dmitry Sytsevich on 5/30/19. +// Copyright © 2019 Dmitry Sytsevich. All rights reserved. +// + +#import + +@interface NSString (URLEncoding) + +- (NSString *)stringByURLEncoding; +- (NSString*)stringBetweenString:(NSString *)start andString:(NSString *)end; + +@end diff --git a/ios/Extensions/NSString+URLEncoding.m b/ios/Extensions/NSString+URLEncoding.m new file mode 100644 index 0000000..d323b58 --- /dev/null +++ b/ios/Extensions/NSString+URLEncoding.m @@ -0,0 +1,34 @@ +// +// NSString+URLEncoding.m +// +// Created by Dmitry Sytsevich on 5/30/19. +// Copyright © 2019 Dmitry Sytsevich. All rights reserved. +// + +#import "NSString+URLEncoding.h" + +@implementation NSString (URLEncoding) + +- (NSString *)stringByURLEncoding { + return (__bridge NSString *)CFURLCreateStringByAddingPercentEscapes(NULL, + (CFStringRef)self, + NULL, + (CFStringRef)@"!*'\"();:@+$,/?%#[]% ", CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding)); +} + +- (NSString*)stringBetweenString:(NSString *)start andString:(NSString *)end { + NSScanner* scanner = [NSScanner scannerWithString:(__bridge NSString * _Nonnull)((__bridge CFStringRef)self)]; + [scanner setCharactersToBeSkipped:nil]; + [scanner scanUpToString:start intoString:nil]; + if ([scanner scanString:start intoString:nil]) + { + NSString* result = nil; + if ([scanner scanUpToString:end intoString:&result]) + { + return result; + } + } + return nil; +} + +@end diff --git a/ios/RNCloudPayments.h b/ios/RNCloudPayments.h index 3c88d5d..d5e052a 100644 --- a/ios/RNCloudPayments.h +++ b/ios/RNCloudPayments.h @@ -1,4 +1,5 @@ #import @interface RNCloudPayments : NSObject + @end diff --git a/ios/RNCloudPayments.m b/ios/RNCloudPayments.m index 20e27be..1414321 100644 --- a/ios/RNCloudPayments.m +++ b/ios/RNCloudPayments.m @@ -1,13 +1,28 @@ #import "RNCloudPayments.h" #import "SDK/Card.m" +#import "SDWebViewController/SDWebViewController.h" +#import "SDWebViewController/SDWebViewDelegate.h" +#import "NSString+URLEncoding.h" + +#define POST_BACK_URL @"https://demo.cloudpayments.ru/WebFormPost/GetWebViewData" + +typedef void (^RCTPromiseResolveBlock)(id result); +typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error); + +@interface RNCloudPayments () + +@property (nonatomic, retain) UINavigationController *navigationController; + +@property (nonatomic) RCTPromiseResolveBlock resolveWebView; +@property (nonatomic) RCTPromiseRejectBlock rejectWebView; + +@end @implementation RNCloudPayments RCT_EXPORT_MODULE(); RCT_EXPORT_METHOD(isValidNumber: (NSString *)cardNumber - cardExp: (NSString *)cardExp - cardCvv: (NSString *)cardCvv resolve: (RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject) { @@ -18,6 +33,17 @@ @implementation RNCloudPayments } }; +RCT_EXPORT_METHOD(isValidExpired: (NSString *)cardExp + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) +{ + if([Card isExpiredValid: cardExp]) { + resolve(@YES); + } else { + resolve(@NO); + } +}; + RCT_EXPORT_METHOD(getType: (NSString *)cardNumber cardExp: (NSString *)cardExp cardCvv: (NSString *)cardCvv @@ -43,4 +69,68 @@ @implementation RNCloudPayments resolve(cryptogram); } + +RCT_EXPORT_METHOD(show3DS: (NSString *)url + transactionId: (NSString *)transactionId + token: (NSString *)token + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) +{ + self.resolveWebView = resolve; + self.rejectWebView = reject; + + // Show WebView + SDWebViewController *webViewController = [[SDWebViewController alloc] initWithURL:url transactionId:transactionId token:token]; + webViewController.m_delegate = self; + self.navigationController = [[UINavigationController alloc] initWithRootViewController:webViewController]; + [self.navigationController.navigationBar setTranslucent:false]; + [[self topViewController] presentViewController:self.navigationController animated:YES completion:nil]; +} + +#pragma MARK: - SDWebViewDelegate + +- (void)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType: (UIWebViewNavigationType)navigationType { + + // Detect url + NSString *urlString = request.URL.absoluteString; + + if ([urlString isEqualToString:POST_BACK_URL]) { + NSString *result = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding]; + NSString *mdString = [result stringBetweenString:@"MD=" andString:@"&PaRes"]; + NSString *paResString = [[result stringBetweenString:@"PaRes=" andString:@""] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; + + NSDictionary *dictionary = @{@"MD": mdString, @"PaRes": paResString}; + + self.resolveWebView(dictionary); + + [self.navigationController dismissViewControllerAnimated:YES completion:nil]; + } +} + +- (void)webViewWillClose:(UIWebView *)webView { + self.rejectWebView(@"", @"", nil); +} + +#pragma MARK: - ViewController + +- (UIViewController *)topViewController { + UIViewController *baseVC = UIApplication.sharedApplication.keyWindow.rootViewController; + if ([baseVC isKindOfClass:[UINavigationController class]]) { + return ((UINavigationController *)baseVC).visibleViewController; + } + + if ([baseVC isKindOfClass:[UITabBarController class]]) { + UIViewController *selectedTVC = ((UITabBarController*)baseVC).selectedViewController; + if (selectedTVC) { + return selectedTVC; + } + } + + if (baseVC.presentedViewController) { + return baseVC.presentedViewController; + } + + return baseVC; +} + @end diff --git a/ios/RNCloudPayments.xcodeproj/project.pbxproj b/ios/RNCloudPayments.xcodeproj/project.pbxproj index 6993c14..31b1b04 100644 --- a/ios/RNCloudPayments.xcodeproj/project.pbxproj +++ b/ios/RNCloudPayments.xcodeproj/project.pbxproj @@ -10,6 +10,8 @@ 954FC90C20AADA700017B273 /* RNCloudPayments.m in Sources */ = {isa = PBXBuildFile; fileRef = 954FC90B20AADA700017B273 /* RNCloudPayments.m */; }; 954FC9F920AC08E60017B273 /* Card.m in Sources */ = {isa = PBXBuildFile; fileRef = 954FC9F620AC08E60017B273 /* Card.m */; }; 954FC9FA20AC08E60017B273 /* NSDataENBase64.m in Sources */ = {isa = PBXBuildFile; fileRef = 954FC9F720AC08E60017B273 /* NSDataENBase64.m */; }; + AE8EA39622A015910033E067 /* SDWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AE8EA39522A015910033E067 /* SDWebViewController.m */; }; + AEB012DB22A1835200B733D5 /* NSString+URLEncoding.m in Sources */ = {isa = PBXBuildFile; fileRef = AEB012DA22A1835100B733D5 /* NSString+URLEncoding.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,6 +34,11 @@ 954FC9F620AC08E60017B273 /* Card.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Card.m; sourceTree = ""; }; 954FC9F720AC08E60017B273 /* NSDataENBase64.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSDataENBase64.m; sourceTree = ""; }; 954FC9F820AC08E60017B273 /* Card.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Card.h; sourceTree = ""; }; + AE8EA39322A015910033E067 /* SDWebViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDWebViewController.h; sourceTree = ""; }; + AE8EA39422A015910033E067 /* SDWebViewDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDWebViewDelegate.h; sourceTree = ""; }; + AE8EA39522A015910033E067 /* SDWebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDWebViewController.m; sourceTree = ""; }; + AEB012D922A1835100B733D5 /* NSString+URLEncoding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+URLEncoding.h"; sourceTree = ""; }; + AEB012DA22A1835100B733D5 /* NSString+URLEncoding.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+URLEncoding.m"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -48,10 +55,12 @@ 954FC8FE20AADA700017B273 = { isa = PBXGroup; children = ( - 954FC9F420AC08D70017B273 /* SDK */, - 954FC90B20AADA700017B273 /* RNCloudPayments.m */, - 954FC90A20AADA700017B273 /* RNCloudPayments.h */, + AEB012D822A1835100B733D5 /* Extensions */, 954FC90820AADA700017B273 /* Products */, + 954FC90A20AADA700017B273 /* RNCloudPayments.h */, + 954FC90B20AADA700017B273 /* RNCloudPayments.m */, + 954FC9F420AC08D70017B273 /* SDK */, + AE8EA39222A015910033E067 /* SDWebViewController */, ); sourceTree = ""; }; @@ -74,6 +83,25 @@ path = SDK; sourceTree = ""; }; + AE8EA39222A015910033E067 /* SDWebViewController */ = { + isa = PBXGroup; + children = ( + AE8EA39422A015910033E067 /* SDWebViewDelegate.h */, + AE8EA39322A015910033E067 /* SDWebViewController.h */, + AE8EA39522A015910033E067 /* SDWebViewController.m */, + ); + path = SDWebViewController; + sourceTree = ""; + }; + AEB012D822A1835100B733D5 /* Extensions */ = { + isa = PBXGroup; + children = ( + AEB012D922A1835100B733D5 /* NSString+URLEncoding.h */, + AEB012DA22A1835100B733D5 /* NSString+URLEncoding.m */, + ); + path = Extensions; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -108,7 +136,7 @@ }; }; buildConfigurationList = 954FC90220AADA700017B273 /* Build configuration list for PBXProject "RNCloudPayments" */; - compatibilityVersion = "Xcode 9.3"; + compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -129,6 +157,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AEB012DB22A1835200B733D5 /* NSString+URLEncoding.m in Sources */, + AE8EA39622A015910033E067 /* SDWebViewController.m in Sources */, 954FC90C20AADA700017B273 /* RNCloudPayments.m in Sources */, 954FC9F920AC08E60017B273 /* Card.m in Sources */, 954FC9FA20AC08E60017B273 /* NSDataENBase64.m in Sources */, @@ -189,7 +219,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -241,7 +271,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.3; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/ios/SDK/Card.h b/ios/SDK/Card.h index 659a3b7..c2dfcbb 100755 --- a/ios/SDK/Card.h +++ b/ios/SDK/Card.h @@ -15,6 +15,8 @@ typedef enum { +(BOOL) isCardNumberValid: (NSString *) cardNumberString; ++(BOOL) isExpiredValid: (NSString *) expiredString; + /** * Create cryptogram * cardNumberString valid card number stirng diff --git a/ios/SDK/Card.m b/ios/SDK/Card.m index b465c13..fcf6ec3 100755 --- a/ios/SDK/Card.m +++ b/ios/SDK/Card.m @@ -127,6 +127,23 @@ +(BOOL) isCardNumberValid: (NSString *) cardNumberString { return ((oddSum + evenSum) % 10 == 0); } ++(BOOL) isExpiredValid: (NSString *) expiredString { + NSArray *cardDateComponents = [expiredString componentsSeparatedByString:@"/"]; + + if ([cardDateComponents[0] integerValue] > 12) { + return NO; + } + + NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitYear fromDate:[NSDate date]]; + int shortYear = [components year] % 100; + + if ([cardDateComponents[1] integerValue] < shortYear) { + return NO; + } + + return YES; +} + -(NSString *) makeCardCryptogramPacket: (NSString *) cardNumberString andExpDate: (NSString *) expDateString andCVV: (NSString *) CVVString andMerchantPublicID: (NSString *) merchantPublicIDString { // ExpDate must be in YYMM format diff --git a/ios/SDWebViewController/SDWebViewController.h b/ios/SDWebViewController/SDWebViewController.h new file mode 100755 index 0000000..198d691 --- /dev/null +++ b/ios/SDWebViewController/SDWebViewController.h @@ -0,0 +1,19 @@ +// +// SDWebViewController.h +// SDWebViewController +// +// Created by Dmitry Sytsevich on 5/30/19. +// Copyright © 2019 Dmitry Sytsevich. All rights reserved. +// + +#import + +@protocol SDWebViewDelegate; +@interface SDWebViewController : UIViewController + +@property (weak, nonatomic) id m_delegate; + +- (id)initWithURL:(id)url transactionId:(NSString *)transactionId token:(NSString *)token; +- (NSString *)stringByURLEncoding; + +@end diff --git a/ios/SDWebViewController/SDWebViewController.m b/ios/SDWebViewController/SDWebViewController.m new file mode 100755 index 0000000..68a1581 --- /dev/null +++ b/ios/SDWebViewController/SDWebViewController.m @@ -0,0 +1,254 @@ +// +// SDWebViewController.m +// SDWebViewController +// +// Created by Dmitry Sytsevich on 5/30/19. +// Copyright © 2019 Dmitry Sytsevich. All rights reserved. +// + +#define IBT_BGCOLOR [UIColor whiteColor] +#define IBT_ADDRESS_TEXT_COLOR [UIColor colorWithRed:.44 green:.45 blue:.46 alpha:1] +#define IBT_PROGRESS_COLOR [UIColor colorWithRed:0 green:.071 blue:.75 alpha:1] + +#define POST_BACK_URL @"https://demo.cloudpayments.ru/WebFormPost/GetWebViewData" + +#import "SDWebViewController.h" +#import "SDWebViewDelegate.h" +#import "NSString+URLEncoding.h" + +@interface SDWebViewController () { + + // Address bar + UIImageView *m_addressBarView; + UILabel *m_addressLabel; + + // URL + NSURL *m_currentUrl; + + BOOL m_bAutoSetTitle; +} +@property (strong, nonatomic) UIWebView *m_webView; + +@property (strong, nonatomic) NSString *m_initUrl; +@property (strong, nonatomic) NSString *m_transactionId; +@property (strong, nonatomic) NSString *m_token; +@property (strong, nonatomic) NSMutableDictionary *m_extraInfo; + +- (void)initWebView; +- (void)initAddressBarView; +- (void)removeAddressBar; +- (void)initNavigationBarItem; + +@end + +@implementation SDWebViewController + +#pragma mark - + +- (id)initWithURL:(id)url transactionId:(NSString *)transactionId token:(NSString *)token { + self = [super init]; + if (!self) { + return nil; + } + + self.m_initUrl = url; + self.m_transactionId = transactionId; + self.m_token = token; + + + if ([url isKindOfClass:[NSString class]]) { + self.m_initUrl = url; + } + else if ([url isKindOfClass:[NSURL class]]) { + self.m_initUrl = [NSString stringWithFormat:@"%@", url]; + } + + m_bAutoSetTitle = YES; + + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.view.backgroundColor = IBT_BGCOLOR; + + if ([self respondsToSelector:@selector(setEdgesForExtendedLayout:)]) { + self.edgesForExtendedLayout = UIRectEdgeNone; + } + + [self initNavigationBarItem]; + [self initAddressBarView]; + [self initWebView]; + + [self loadURL:self.m_initUrl transactionId:self.m_transactionId token:self.m_token]; +} + +- (void)didReceiveMemoryWarning { + [super didReceiveMemoryWarning]; + // Dispose of any resources that can be recreated. +} + +- (void)dealloc { + [self.m_webView stopLoading]; + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; + self.m_webView.delegate = nil; + + m_addressBarView = nil; + m_addressLabel = nil; + + m_currentUrl = nil; +} + +#pragma MARK: - Private Method + +- (void)initWebView { + self.m_webView = [[UIWebView alloc] initWithFrame:self.view.bounds]; + self.m_webView.backgroundColor = [UIColor clearColor]; + self.m_webView.delegate = self; + self.m_webView.scalesPageToFit = YES; + self.m_webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + [self.view addSubview:self.m_webView]; +} + +- (void)updateDisplayTitle:(NSString *)nsTitle { + self.title = nsTitle; +} + +#pragma MARK: - Address Bar + +- (NSString *)getAddressBarHostText:(NSURL *)url { + if ([url.host length] > 0) { + return [NSString stringWithFormat:NSLocalizedString(@"Provided by %@", nil), url.host]; + } else { + return @""; + } +} + +- (void)initAddressBarView { + if (!m_addressBarView) { + m_addressBarView = [[UIImageView alloc] init]; + m_addressBarView.frame = (CGRect){ + .origin.x = 0, + .origin.y = 0, + .size.width = CGRectGetWidth(self.view.bounds), + .size.height = 40 + }; + + m_addressLabel = [[UILabel alloc] init]; + m_addressLabel.frame = CGRectInset(m_addressBarView.bounds, 10, 6); + m_addressLabel.textColor = [UIColor clearColor]; + m_addressLabel.textAlignment = NSTextAlignmentCenter; + m_addressLabel.textColor = IBT_ADDRESS_TEXT_COLOR; + m_addressLabel.font = [UIFont systemFontOfSize:12]; + + [m_addressBarView addSubview:m_addressLabel]; + } + + [self.view addSubview:m_addressBarView]; +} + +- (void)removeAddressBar { + [m_addressBarView removeFromSuperview]; + m_addressBarView = nil; + m_addressLabel = nil; +} + +#pragma MARK: - Navigation Bar + +- (void)initNavigationBarItem { + UIBarButtonItem *backItem = + [[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"Done", nil) + style:UIBarButtonItemStylePlain + target:self + action:@selector(onCloseAction:)]; + + self.navigationItem.rightBarButtonItems = @[ backItem ]; + +} + +- (void)onCloseAction:(__unused id)sender { + if ([_m_delegate respondsToSelector:@selector(webViewWillClose:)]) { + [_m_delegate webViewWillClose:self.m_webView]; + } + + [self.presentingViewController dismissViewControllerAnimated:YES completion:NULL]; +} + +#pragma MARK: - WebView Action + +- (BOOL)isTopLevelNavigation:(NSURLRequest *)req { + if (req.mainDocumentURL) { + return [req.URL isEqual:req.mainDocumentURL]; + } else { + return YES; + } +} + +- (void)loadURL:(NSString *)url transactionId:(NSString *)transactionId token:(NSString *)token { + NSString *body = [NSString stringWithFormat: @"MD=%@&PaReq=%@&TermUrl=%@", token, transactionId, POST_BACK_URL]; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString: url]]; + [request setHTTPMethod: @"POST"]; + body = [body stringByURLEncoding]; + [request setHTTPBody: [body dataUsingEncoding: NSUTF8StringEncoding]]; + [self.m_webView loadRequest: request]; +} + +#pragma MARK: - UIWebViewDelegate + +- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { + + if ([_m_delegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) { + [_m_delegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; + } + + m_currentUrl = request.mainDocumentURL; + m_addressLabel.text = [self getAddressBarHostText:m_currentUrl]; + + return YES; +} + +- (void)webViewDidStartLoad:(UIWebView *)webView { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; + + if ([_m_delegate respondsToSelector:@selector(onWebViewDidStartLoad:)]) { + [_m_delegate onWebViewDidStartLoad:webView]; + } + + if ([self isTopLevelNavigation:webView.request]) { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; + } +} + +- (void)webViewDidFinishLoad:(UIWebView *)webView { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; + + if ([_m_delegate respondsToSelector:@selector(onWebViewDidFinishLoad:)]) { + [_m_delegate onWebViewDidFinishLoad:webView]; + } + + if ([self isTopLevelNavigation:webView.request]) { + m_currentUrl = webView.request.mainDocumentURL; + m_addressLabel.text = [self getAddressBarHostText:m_currentUrl]; + + if (m_bAutoSetTitle) { + NSString *nsTitle = [webView stringByEvaluatingJavaScriptFromString:@"document.title"]; + [self updateDisplayTitle:nsTitle]; + } + } +} + +- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error { + [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO]; + + if ([_m_delegate respondsToSelector:@selector(webViewFailToLoad:)]) { + [_m_delegate webViewFailToLoad:error]; + } + + if ([error code] != NSURLErrorCancelled && + [self isTopLevelNavigation:webView.request]) { + + } +} + +@end diff --git a/ios/SDWebViewController/SDWebViewDelegate.h b/ios/SDWebViewController/SDWebViewDelegate.h new file mode 100755 index 0000000..f505c5d --- /dev/null +++ b/ios/SDWebViewController/SDWebViewDelegate.h @@ -0,0 +1,20 @@ +// +// SDWebViewDelegate.h +// SDWebViewController +// +// Created by Dmitry Sytsevich on 5/30/19. +// Copyright © 2019 Dmitry Sytsevich. All rights reserved. +// + +#import + +@protocol SDWebViewDelegate + +@optional +- (void)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType: (UIWebViewNavigationType)navigationType; +- (void)webViewWillClose:(UIWebView *)webView; +- (void)onWebViewDidFinishLoad:(UIWebView *)webView; +- (void)onWebViewDidStartLoad:(UIWebView *)webView; +- (void)webViewFailToLoad:(NSError *)error; + +@end