Horizon Wallet & Terminal

SDK Documentation

The Horizon SDK for iOS makes it quick and easy to build an excellent payment experience in your iOS app. We provide a powerful workflow and APIs that enables your UI to complete the full customer experience.

Horizon SDK has been built as a private pod.
Minimum deployment target: iOS 11.0.
In order to be able to use Wallet SDK in projects the first step is to install cocoapods:
sudo gem install cocopods.

How to add horizonSDK to the project

  1. Create a Podfile in project using this command:
    pod init
  2. Within Podfile put at the top:
    source 'https://gitlab.com/vipaso/horizon/cocoapods-specs'
  3. Enter next command in terminal in order to fetch specs files:
    pod repo add horizon-cocoapods-specs 'https://gitlab.com/vipaso/horizon/cocoapods-specs'
  4. Below this line, for specific app target put:
    pod 'horizonSDK'
  5. Our SDK uses AFNetworking library 2.6 version so in order to be able to submit the app on AppStore add next pre_install script in Podfile:
# Workaround to remove all UIWebView references from AFNetworking(later AFNetworking should be migrated to the latest version)
pre_install do |installer|
  puts 'pre_install begin....'
  dir_af = File.join(installer.sandbox.pod_dir('AFNetworking'), 'UIKit+AFNetworking')
  Dir.foreach(dir_af) {|x|
    real_path = File.join(dir_af, x)
    if (!File.directory?(real_path) && File.exists?(real_path))
      if((x.start_with?('UIWebView') || x == 'UIKit+AFNetworking.h'))
        File.delete(real_path)
        puts 'delete:'+ x
      end
    end
  }
  puts 'end pre_install.'
end
  1. In order to be able to run app on M1 machines on iOS simulators add next post_install script in Podfile:
post_install do |installer|

  # Path CocoaPods targets build settings
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|

    config.build_settings['SWIFT_VERSION'] = '5.0'
    config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64"
    
    end
  end
end
  1. Run pod install.

Afterwards, use .xcworkspace file instead of .xcodeproj.

How to update horizonSDK in the project

  1. Run pod update command.

How to use horizonSDK in the project

Wallet

There are 3 main clients in SDK for Wallet part which should be instantiated from the project level in order to be able to use SDK.
If one of them is missing (not configured), the app will crash.
Main clients are:

  1. AccountDataClient
  2. WalletClient
  3. PaymentClient

They can be configured via horizonSDK class.
horizonSDK class is the main class in SDK used for setup:
AccountDataClient, WalletClient and PaymentClient.

Before instantiating AccountDataClient we need to instantiate 2 clients which should be injected in AccountDataClient:

  1. IdentityClient
  2. IdentityVerificationClient.

IdentityClient
This client handles all registrations and account activations. With this client we can request to start the registration procedure, set first pin, activate pin, set touch id and etc...

Instantiating IdentityClient:

@property (nonatomic, strong, readwrite) IdentityClient *identityClient;
@property (nonatomic, strong, readwrite) TouchIDSupport *touchIDSupport;

self.touchIDSupport = [TouchIDSupport new];
[self.touchIDSupport start];

self.identityClient = [IdentityClient sharedInstanceWallet];
self.identityClient.codeSettingsConvenience = [IdentityClientCodeSettingConveniences conveniencesWithIdentityClient:self.identityClient touchIDSupport:self.touchIDSupport];

[self.identityClient start];

Usage of IdentityClient

With this client registration and restoration of a user can be started, a pin code can be set, the app can be locked and unlocked and touch/face ID can be used.

In order to disable locking device you can simple use these methods:

- (void)deactivatePin;
- (BOOL)setTouchIDOn:(BOOL)on error:(NSError **)error;

// Examples of calling
[self.identityClient setTouchIDOn:NO error:nil];
[self.identityClient deactivatePin];

IdentityVerificationClient
This client handles the verification of identity, so when the user registers in the app, he needs to confirm (verify) his identity.

Instantiating IdentityVerificationClient:

@property (nonatomic, strong, readwrite) IdentityVerificationClient *identityVerificationClient;

self.identityVerificationClient = [[IdentityVerificationClient alloc] initWithIdentityClient:self.identityClient];

Cache
Our SDK also supports caching. There are two different types: persistent and temporary.
Cache will later be used for injecting in different clients: AccountDataClient, WalletClient, and etc...

Instantiating Cache:

@property (nonatomic, strong, readwrite) Cache *cache;

// Depends if we need to use persistent cache or temporary cache
if ([[NSUserDefaults standardUserDefaults] boolForKey:KKWApplicationConfigurationFeaturePersistentCacheEnabled]) {
    self.cache = [Cache persistentCache];
} else {
    self.cache = [Cache temporaryAnonymousCache];
}

Registration and reactivation of a consumer

We provide a straight forward registration process and support the a reactivation of a wallet as well. The consumer receives a registration code per SMS and a link per mail.

This shows how the app could look like:

Registration process can be started with one of these two methods:

- (void)requestStartRegistrationProcedure;
- (void)requestStartPinlessRegistrationProcedure;

Usually second one should be use if you want to start registration with disabled pin code.

Next step is to setup user data and start registration process with those that and invitation code.
Invitation code is optional here.
You can use first two methods, or third one that is shortcut for first two methods.

- (void)prepareWalletUserData:(WalletUserData *)walletUserData;
- (void)registerWithInvitationCode:(NSString *)invitationCode;

- (void)registerWithWalletUserData:(WalletUserData *)walletUserData invitationCode:(NSString *)invitationCode; // short-cut for the above two

You can skip invitation code step with:

- (void)cancelEnteringInvitationCode;

Next step is to send puk code to server that is received on phone number entered from first step.

- (void)requestRegistrationWithPuk:(NSString *)puk;

User also need to confirm his email in order to activate account.
After this step, wallet activation is called automatically and identity client is formed. User can access to the app.
If user didn't confirm email yet, you can always check activation of account calling method:

- (void)retryCheckActivationAgain;

After this you continue with sending user email and then confirming puk code

- (void)requestRestorationPukWithEmail:(NSString *)email;
- (void)requestRestorationWithPuk:(NSString *)puk;

Restoration process is similar like registration process.
Only difference is first step, where you send only email.
You start restoration process with one of those two methods(same principle as for registration):

- (void)requestPinlessRestoration;
- (void)requestRestoration;

After this reactivation step is called automatically but also you can handle it from universal link.

- (void)reactivateWithReactivationToken:(NSString *)reactivationToken;
- (void)reactivatePinlessWithReactivationToken:(NSString *)reactivationToken;

Add Payment Methods

with our SDK the consumer is able to add multiple methods of payment to your app. The following example will focus on adding a credit card.

WalletClient
This client handles everything related to credit cards. For example: adding credit cards, removing credit cards, updating credit cards, fetch all credit cards for authenticated user.

In order to instantiate WalletClient we need to instantiate a few other clients, CustomStatusMessageManager and NetworkReachability:

  1. BonusProgramClient
  2. GoldProgramMembershipsClient
  3. CouponProgramClient
  4. DailyTokenClient
    Instantiation of those clients you can found below on topics Handling of backend messages and network status and Loyalty Program**

Instantiating:

@property (nonatomic, strong, readwrite) WalletClient *walletClient;

self.walletClient = [[WalletClient alloc] initWithIdentityClient:self.identityClient identityVerificationManager:self.identityVerificationClient bonusProgramClient:self.bonusProgramClient goldProgramMembershipsClient:self.goldProgramMembershipsClient couponProgramClient:self.couponProgramClient customStatusMessageManager:self.customStatusMessageManager dailyTokenClient:self.dailyTokenClient cache:self.cache];
self.walletClient.networkReachability = self.networkReachability;
[WalletClientSDK.sharedInstance setupWalletClient:self.walletClient];

[self.walletClient start];

Usage of WalletClient

With this client you can get, add, remove and update credit card.

You can retrieve and get user wallet, credit cards with this method:

- (id<Cancellable>)enqueueGetWalletWithCompletion:(WalletClientCompletion)completion;

Update and remove credit card can be done with those two methods(api calls):

- (id<Cancellable>)enqueueUpdateCard:(PaymentCard *)card withStatus:(BOOL)status cardName:(NSString *)cardName completion:(CardActivationCompletion)completion;
- (id<Cancellable>)enqueueRemoveCardWithID:(KKW_ID)cardID completion:(CardRemoveCompletion)completion;

Add credit card process is more complex and can be done with next flow.
First you need to execute api call and send credit card details data to server. You can do it with this method:

- (void)enqueueAddPaymentCardWithID:(KKW_ID)cardTypeID cardholder:(NSString *)cardholder cardNumber:(NSString *)cardNumber cvc:(NSString *)cvc expDate:(NSString *)expDate completion:(WalletClientAddCardCompletion)completion;

From this api call AddCreditCardPayload object will be stored in paymentPayload property.
You use data from this property to finish add credit card process.
In this class you have PaymentDetails object that handle data for 3DS of credit card.
You can retrieve those that with this example:

[WalletClient sharedInstance].paymentPayload.paymentData

Check property card3DSNotEnrolled to determinate if credit card need to pass 3DS flow.
If yes you need to load POST request in web view with url from redirectionUrl property.
Body parameters for this request you can get from method(you need to pass boundary):
Example for boundary(string): "----WebKitFormBoundary7MA4YWxkTrZu0gW"

- (NSMutableData *)getJsonBodyDataWithBoundary:(NSString *)boundary;

You need to add header value for this request also:

request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

To handle result of web view POST request and to finish add credit card process you have to do next:

  • You need to fetch "MD" and "PaRes" parameters from response of web view request(in code snippet example we fetch those parameters calling JS script).
  • You will be redirected on url from this property postDataTermUrl.
    Example of web view listener method:
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        if let url = webView.url?.absoluteString, let termUrl = paymentDetails?.postDataTermUrl {
            if url == termUrl { // Continue only if you are redirected on term url(postDataTermUrl)
                let jsCode = "var values={};" +
                    "var form = document.getElementsByTagName('form').length > 0 ? document.getElementsByTagName('form')[0] : null;" +
                    "console.log('RESULT 1='+form.elements);" +
                    "function results(form) {" +
                    "for(var i=0 ; i< form.elements.length; i++){" +
                    "   values[form.elements[i].name] = form.elements[i].value;" +
                    "}" +
                    "return values;" +
                    "}" +
                    "if (form) results(form);"
                
                self.webView.evaluateJavaScript(jsCode, completionHandler: { (result, error) in
                    if let dictionary = result as? NSDictionary {
                        guard let md = dictionary.value(forKey: "MD") as? String, let paRes = dictionary.value(forKey: "PaRes") as? String else {
                          	// TODO: Display error
                            return
                        }
                        // TODO: Execute add credit card with parameters md and paRes
                    }
                })
            }
        }
    }

Finally you can call last api call and finish the add credit cards process.
The parameters for this api call you should have retrieved from the previous step.
You also pass PaymentDetails as parameter retrieved from add credit card api call from the first step.
You can now finish the process by calling this method from WalletClient:

- (void)enqueueExecuteAddPaymentCardWithPaymentDetails:(PaymentDetails *)paymentDetails md:(NSString *)md paRes:(NSString *)paRes completion:(WalletClientAddCardCompletion)completion;

Payment Process

Our SDK allows a simple and intuitive payment process. The consumer can select the payment method of choice and add a tip to the payment. This is what it could look like in the app:

PaymentClient
This client handles everything related to payments.

Instantiating:

@property (nonatomic, strong, readwrite) PaymentClient *paymentClient;

self.paymentClient = [PaymentClient instanceWithIdentityClient:self.identityClient dailyTokenClient:self.dailyTokenClient bonusProgramClient:self.bonusProgramClient paymentCardIDProvider:self.walletClient]; // is 'restarted' in RootViewController
[WalletClientSDK.sharedInstance setupPaymentClient:self.paymentClient];

Usage of PaymentClient and ManualConnectionDecider

This client and decider is used for payment process.
In order to follow bluetooth activity for payment, bonus program redemption, coupon redemption, you will need to initialise and form ManualConnectionDeciderProtocol object. With this protocol you can track and confirm bluetooth activity and connection with other devices.
Example of init:

NSSet *deciderSet = [NSSet setWithArray:@[
            [[self.paymentClient fusionTracker] manualConnectionDecider],
            [self.bonusRedemptionClient manualConnectionDecider],
            [self.goldRedemptionClient manualConnectionDecider],
            [self.couponRedemptionClient manualConnectionDecider]
        ]];

id<ManualConnectionDeciderProtocol> manualConnectionDecider = [ManualConnectionDeciderCollection collectionWithManualConnectionDeciders:deciderSet];

We pass this decider to our view that handle user actions for pairing with another device in order to pay, redeem bonus or coupon program.
You need to track ManualConnectionDeciderState in KVO controller in order to display proper view and handle next user actions.
Example:

self.paymentClientManualConnectionDecider = [[self.paymentClient fusionTracker] manualConnectionDecider];
        
@weakify(self);
[self.KVOControllerNonRetaining observe:self keyPath:@keypath(self.paymentClientManualConnectionDecider.state) options:NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
    dispatch_async(dispatch_get_main_queue(), ^{
        @strongify(self);

        ManualConnectionDeciderState state = self.paymentClientManualConnectionDecider.state;
        if (state == ManualConnectionDeciderStateWaitingForCandidate) {
            [self dismissPaymentIfNeeded];
        } else if (state == ManualConnectionDeciderStateAccepted) {
            WalletPaymentViewControllerNew *paymentViewController = (WalletPaymentViewControllerNew *)self.paymentNavigationController.topViewController;
            [paymentViewController startPaymentIfPossible];
        }
        NSLog(@"ManualConnectionDeciderState: %ld", state);
    });
}];

[self.KVOControllerNonRetaining observe:self keyPath:@keypath(self.paymentClientManualConnectionDecider.candidate) options:NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
    dispatch_async(dispatch_get_main_queue(), ^{
        @strongify(self);

        PeripheralTracking *tracking = NSOBJECT_DYNAMIC_CAST(self.paymentClientManualConnectionDecider.candidate, PeripheralTracking);
        KKW_ID sessionID = (KKW_ID)tracking.peripheralMetaData.additionalIdentifier.longLongValue;

        if (sessionID > 0 && self.paymentNavigationController == nil) {
            [self.identityClient.requestManager enqueueGetPaymentStateWithPosSessionID: (KKW_SessionID)sessionID
                                                                            completion:^(PaymentStatePayload *payload, NSError *requestError) {

                [self.paymentClientManualConnectionDecider startDeciding];
                [self showPaymentIfNecessaryWithPaymentOrder:payload.paymentOrder merchant:payload.merchant animated:YES];
            }];
        }
    });
}];

API that you can use for this protocol:

// API for view, can be called on main queue (may be async)
- (void)startDeciding;
- (void)decideForCandidate;
- (void)decideAgainstCandidate;
- (void)restart; // restart after completion

Call startDeciding method when you want to take ownership of current candidate for pairing.
decideForCandidate method should be called when user accept connection with device, for example accept payment or redemption of bonus program.
decideAgainstCandidate should be called when user denied connection.

In order to get merchant info about the payment we need to call getPaymentState API and in the response we need to tell ManualConnectionDecider object to start deciding and show payment screen.

[self.identityClient.requestManager enqueueGetPaymentStateWithPosSessionID: (KKW_SessionID)sessionID
                                                                                    completion:^(PaymentStatePayload *payload, NSError *requestError) {
                        
    [self.paymentClientManualConnectionDecider startDeciding];
    [self showPaymentIfNecessaryWithPaymentOrder:payload.paymentOrder merchant:payload.merchant animated:YES];
}];

When user wants to execute payment(click on "Pay" button or something else) we first need to tell ManualConnectionDecider to decide for candidate using next method:

[self.paymentClientManualConnectionDecider decideForCandidate];

We can observe state of ManualConnectionDecider object using next code and if it's accepted then we need to start connection to payment session:

[self.KVOControllerNonRetaining observe:self keyPath:@keypath(self.paymentClientManualConnectionDecider.state) options:NSKeyValueObservingOptionNew block:^(id observer, id object, NSDictionary *change) {
    dispatch_async(dispatch_get_main_queue(), ^{
        @strongify(self);

        ManualConnectionDeciderState state = self.paymentClientManualConnectionDecider.state;
        if (state == ManualConnectionDeciderStateWaitingForCandidate) {
            [self dismissPaymentIfNeeded];
        } else if (state == ManualConnectionDeciderStateAccepted) {
            WalletPaymentViewControllerNew *paymentViewController = (WalletPaymentViewControllerNew *)self.paymentNavigationController.topViewController;
            [paymentViewController startPaymentIfPossible]; // connect to payment session
        }
        NSLog(@"ManualConnectionDeciderState: %ld", state);
    });
}];

In order to connect to payment session we need to call next method on PaymentClient

- (void)connectToPaymentSession;

Last step of execution of payment should be calling PaymentClient method(pass confirm parameter and tip).
This method could be executed in onHostConnectedCallback method on PaymentClient*, or later by some user action(click on button for example). onHostConnectedCallback** method inform us that devices are connected and ready for execution of payment.

paymentClient?.onHostConnectedCallback = { [weak self] in
    guard let self = self else { return }
    self.paymentClient?.setOrderConfirmedByUser(true, tip: self.tip)
}

If PaymentClient enters at state PaymentClientStateCompletedWithError, that means some error occurred. You can display the error message and then you should restart PaymentClient and ManualConnectionDecider
Example:

self.paymentClient.humanizedError // Get error from this property

[self.paymentClient restart]; // Restart payment client
[self.paymentClient.fusionTracker.manualConnectionDecider restart]; // restart decider

If client enters at state PaymentClientStateAcquiringPaymentExecution, that means the payment process is started successfully and you can display to user payment animation for example.
After that when client enters at state PaymentClientStateCompleted, the payment process is finished, you can display message or view to user and finish ui part for payment success.
You can also display info regarding bonus program and points earned with this payment.
Take this data from bonusProgramMembership property of PaymentClient.
At the end you should also restart client.

[self.paymentClient restart]; // Restart payment client

Last step is the notification that is send when payment is finished, PaymentClientFinishedAPaymentNotification.
You should already subscribe to this notification in init phase of payment client. Check Start Observing Payment and Redemption at the top of document.

Show user data, transactions and details

The following use cases are focusing on displaying and updating the user as well as presenting transactions and their details.

AccountDataClient
This client handles everything account-related in SDK.
In example you can:
Get account data using the method:

- (id<Cancellable>)enqueueGetAccountData:(void (^)(WalletUserData *walletUserData, NSError *error))completion;

Set account data using the method:

- (void)enqueueSetAccountDataWithWalletUserData:(WalletUserData *)walletUserData completion:(void (^)(WalletUserData *walletUserData, NSError *error))completion;

Instantiating AccountDataClient:

@property (nonatomic, strong, readwrite) AccountDataClient *accountDataClient;

self.accountDataClient = [AccountDataClient accountDataClientWithIdentityClient:self.identityClient identityVerificationManager:self.identityVerificationClient cache:self.cache];
[WalletClientSDK.sharedInstance setupAccountDataClient:self.accountDataClient];

self.identityVerificationClient.accountDataClient = self.accountDataClient;

Usage of AccountDataClient
With this client wallet user data can be retrieved and updated.

- (id<Cancellable>)enqueueGetAccountData:(void (^)(WalletUserData *walletUserData, NSError *error))completion;
- (void)enqueueSetAccountDataWithWalletUserData:(WalletUserData *)walletUserData completion:(void (^)(WalletUserData *walletUserData, NSError *error))completion;

WalletTransactionsClient
This client handles everything related to transactions, for example getting transactions, transaction details etc...

Instantiating:

@property (nonatomic, strong, readwrite) WalletTransactionsClient *transactionsClient;

self.transactionsClient = [WalletTransactionsClient clientWithIdentityClient:self.identityClient cache:self.cache];

Loyalty Program

An additional feature of the SDK is the loyalty program which enables you to integrate bonus points for your consumers.

BonusProgramClient
This client handles everything that is related to the bonus program.

Instantiating:

@property (nonatomic, strong, readwrite) BonusProgramClient *bonusProgramClient;

self.bonusProgramClient = [BonusProgramClient bonusProgramClientWithIdentityClient:self.identityClient cache:self.cache];

GoldProgramMembershipsClient
This client handles everything related to the gold program.

Instantiating:

@property (nonatomic, strong, readwrite) GoldProgramMembershipsClient *goldProgramMembershipsClient;

self.goldProgramMembershipsClient = [[GoldProgramMembershipsClient alloc] initWithIdentityClient:self.identityClient cache:self.cache];

CouponProgramClient
This client handles everything related to the coupon program.

Instantiating:

@property (nonatomic, strong, readwrite) CouponProgramClient *couponProgramClient;

self.couponProgramClient = [CouponProgramClient couponProgramClientWithIdentityClient:self.identityClient cache:self.cache];

DailyTokenClient
This client handles everything related to the daily token. This means it is used for refreshing the token on a daily basis.

Next is instantiating redemption clients:

@property (nonatomic, strong, readwrite) DailyTokenClient *dailyTokenClient;

self.dailyTokenClient = [DailyTokenClient dailyTokenClientWithIdentityClient:self.identityClient cache:self.cache];

Bonus Redemption Client

@property (nonatomic, strong, readwrite) RedemptionClient *bonusRedemptionClient;

self.bonusRedemptionClient = [RedemptionClient redemptionClientForBonusRedemptionWithIdentityClient:self.identityClient dailyTokenClient:self.dailyTokenClient bonusProgramClient:self.bonusProgramClient];

Gold Redemption Client

@property (nonatomic, strong, readwrite) RedemptionClient *goldRedemptionClient;

self.goldRedemptionClient = [RedemptionClient redemptionClientForGoldRedemptionWithIdentityClient:self.identityClient dailyTokenClient:self.dailyTokenClient bonusProgramClient:self.bonusProgramClient];

Coupon Redemption Client

@property (nonatomic, strong, readwrite) RedemptionClient *couponRedemptionClient;

self.couponRedemptionClient = [RedemptionClient redemptionClientForCouponRedemptionWithIdentityClient:self.identityClient dailyTokenClient:self.dailyTokenClient bonusProgramClient:self.bonusProgramClient];

PaymentAndRedemptionClientCoordinator
This coordinator handles payments and redemptions.

Instantiating:

@property (nonatomic, strong, readwrite) PaymentAndRedemptionClientCoordinator *paymentAndRedemptionClientCoordinator;

self.paymentAndRedemptionClientCoordinator = [[PaymentAndRedemptionClientCoordinator alloc] initWithPaymentClient:self.paymentClient bonusRedemptionClient:self.bonusRedemptionClient goldRedemptionClient:self.goldRedemptionClient couponRedemptionClient:self.couponRedemptionClient walletClient:self.walletClient networkReachability:self.networkReachability bonusProgramClient:self.bonusProgramClient identityClient:self.identityClient];
[self.paymentAndRedemptionClientCoordinator start];

Usage of BonusProgramClient
This client is handling bonus program related stuff: participate to some bonus program, get list of all bonus programs, get list of participating bonus program, update bonus program membership and etc...

Instantiating BonusProgramClient:

@property (nonatomic, strong, readwrite) BonusProgramClient *bonusProgramClient;

self.bonusProgramClient = [BonusProgramClient bonusProgramClientWithIdentityClient:self.identityClient cache:self.cache];

To get all bonus programs use property allBonusProgramsVehicle on BonusProgramClient:

@property (nonatomic, readonly) BonusProgramsVehicle *allBonusProgramsVehicle;

To get participating bonus programs use property participatingBonusProgramsVehicle on BonusProgramClient:

@property (nonatomic, readonly) BonusProgramsVehicle *participatingBonusProgramsVehicle;

To fetch fresh bonus programs(ignoring cache) use updateBonusProgramMembershipsWithCompletion method on BonusProgramClient:

- (void)updateBonusProgramMembershipsWithCompletion:(nullable void (^)(void))completion;

To fetch bonus programs if needed(if cache is not fresh) use updateBonusProgramMembershipsIfNeeded method on BonusProgramClient:

- (void)updateBonusProgramMembershipsIfNeeded;

To update bonus program membership with some state use next method on BonusProgramClient:

- (void)updateBonusProgramMembership:(BonusProgramMembership *)bonusProgramMembership withParticipationState:(BonusProgramMembershipState)state completionHandler:(void (^)(BonusProgramMembership *, NSError *))completionHandler;

Handling of backend messages and network status

CustomStatusMessageManager
This manager handles the custom message from the backend.
It parses the data from backend and tells the client to show alerts or some kind of alert or notification in the app which should be shown to the user.

Instantiating:

@property (nonatomic, strong, readwrite) CustomStatusMessageManager *customStatusMessageManager;

self.customStatusMessageManager = [CustomStatusMessageManager new];

NetworkReachability
Handles network status.

Instantiating:

@property (nonatomic, strong, readwrite) NetworkReachability *networkReachability;

self.networkReachability = [NetworkReachability sharedInstance];

RequestProxyClientCoordinator

Instantiating:

@property (nonatomic, strong, readwrite) RequestProxyClientCoordinator *requestProxyClientCoordinator;

self.requestProxyClientCoordinator = [[RequestProxyClientCoordinator alloc] initWithProxyClientConnectors:@[
  (id<ProxyClientConnector>)self.paymentClient.connector,
  (id<ProxyClientConnector>)self.bonusRedemptionClient.connector,
  (id<ProxyClientConnector>)self.goldRedemptionClient.connector,
  (id<ProxyClientConnector>)self.couponRedemptionClient.connector
]];

self.requestProxyClientCoordinator.shouldUseProxyDueToNetworkProblem = ^BOOL {
  return [NetworkReachability sharedInstance].reachabilityStatus != NetworkReachabilityStatusFast;
};
self.identityClient.requestManager.requestProxyClientCoordinator = self.requestProxyClientCoordinator;
[self.requestProxyClientCoordinator configureRequestProxyClient];

Observers

At the end we need to add several observers in order to complete our communication with SDK:

Start Observing Application

#pragma mark - Observe Application

- (void)startObservingApplication
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleApplicationWillEnterForegroundNotification2:) name:ApplicationWillEnterForegroundNotification(2) object:nil];
}

- (void)handleApplicationWillEnterForegroundNotification2:(NSNotification *)notification
{
    [self.touchIDSupport handleApplicationWillEnterForeground];
    [self.walletClient handleApplicationWillEnterForegroundAfterBackgroundTime:[notification.userInfo[ApplicationWillEnterForegroundNotificationDurationInBackgroundKey] doubleValue]];
}

Start Observing Identity Client

#pragma mark - IdentityClient Observation

- (void)startObservingIdentityClient
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIdentityClientDidInvalidateIdentityNotification:) name:IdentityClientDidInvalidateIdentityNotification object:self.identityClient];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIdentityClientIdentityDidGetReadyNotification:) name:IdentityClientIdentityDidGetReadyNotification object:nil];
}

- (void)handleIdentityClientDidInvalidateIdentityNotification:(NSNotification *)notification
{
    [self.cache reset:^{
        [self.bonusProgramClient reset];
        [self.goldProgramMembershipsClient reset];
        [self.transactionsClient reset];
        [self.walletClient reset];
        [self.identityVerificationClient reset];
        [self.customStatusMessageManager reset];
        [self.accountDataClient reset];
        [self.dailyTokenClient reset];
    }];
}

- (void)handleIdentityClientIdentityDidGetReadyNotification:(NSNotification *)notification
{
    [self.walletClient handleIdentityClientIdentityDidGetReady];
}

Start Observing Network

- (void)startObservingNetwork
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNetworkMightWorkAgainNotification:) name:NetworkMightWorkAgainNotification object:nil];
}

- (void)handleNetworkMightWorkAgainNotification:(NSNotification *)notification
{
    [self.transactionsClient handleNetworkMightWorkAgain];
    [self.walletClient handleNetworkMightWorkAgain];
    [self.merchantMapClient handleNetworkMightWorkAgain];
    [self.bonusProgramClient handleNetworkMightWorkAgain];
}

Start Observing Payment and Redemption

- (void)startObservingPaymentAndRedemption
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handlePaymentClientFinishedAPaymentNotification:) name:PaymentClientFinishedAPaymentNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleRedemptionClientFinishedARedemptionNotification:) name:RedemptionClientFinishedARedemptionNotification object:nil];
}

- (void)handlePaymentClientFinishedAPaymentNotification:(NSNotification *)notification
{
    [self.transactionsClient handlePaymentClientFinishedAPayment];
    [self.walletClient handlePaymentClientFinishedAPayment];
}

- (void)handleRedemptionClientFinishedARedemptionNotification:(NSNotification *)notification
{
    [self.walletClient handleRedemptionClientFinishedARedemption];
}

In dealloc just simply stop observing:

- (void)stopObserving
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [self.customStatusMessageManager stopObserving];
    [self.walkthroughPresenter stopObservingNotifications];
}

Merchant(Terminal)

In Merchant part there is one main client which should be instantiated in the beginning and it's called:
MerchantServiceClient

MerchantServiceClient

In order to instantiate this client we need also IdentityClient to be instantiated first. Before we actually instantiate IdentityClient we need also to have the instance of TouchIDSupport class.

The following code will explain how to achieve these things:

@property (nonatomic, strong, readwrite) TouchIDSupport *touchIDSupport;
@property (nonatomic, strong, readwrite) IdentityClient *identityClient;
@property (nonatomic, strong, readwrite) MerchantServiceClient *merchantServiceClient;

self.touchIDSupport = [TouchIDSupport new];
[self.touchIDSupport start];

self.identityClient = [IdentityClient sharedInstanceTerminal];
self.identityClient.codeSettingsConvenience = [IdentityClientCodeSettingConveniences conveniencesWithIdentityClient:self.identityClient touchIDSupport:self.touchIDSupport];
[self.identityClient start];

self.merchantServiceClient = [MerchantServiceClient clientWithIdentityClient:self.identityClient];
[self.merchantServiceClient start];

This can be instantiated in AppDelegate or in the first view controller or in some ViewModel class and later can be pass to another controllers using DI.

In order to activate account we need to use IdentityClient object.

[self.identityClient requestRegisterActivationWithMerchantEmail:email merchantPassword:password];

Listening on IdentityClientstate property we can execute different operations:

[self.KVOControllerNonRetaining observe:self keyPath:@keypath(self.identityClient.state) options:0 block:^(id observer, id object, NSDictionary *change) {
    @strongify(self);

    dispatch_async(dispatch_get_main_queue(), ^{
        if (self.presentedViewController) {
            // prevent popping and pushing while in the midst of presenting something else (this will fail with an UIKit warning)
            // The case when this happens is, when an alert is shown for an error and at the same time a state change happens.
            return;
        }

        [self updateViewControllerForState:self.identityClient.state asRootViewController:NO];
    });
}];

For different states we need to perform different actions. The following code is describing that:

- (void)updateViewControllerForState:(IdentityClientState)state asRootViewController:(BOOL)asRoot
{
    DDLogInfo(@"%@: updateViewControllerForState: %@%@", NSStringFromClass(self.class), stringFromIdentityClientState(state), asRoot ?@" (as root)": @"");
    
    [[OverlayManager sharedInstance] removeOverlayViewsOnViewController:self animated:YES completion:nil];
    [self updateTaskFromIdentityClientState];
    
    NSString *overlayTitle = nil;

    switch (state) {
        case IdentityClientStateAwaitingUserRegistrationDecision:
        {
            if (self.identityClient.identityClientType == IdentityClientTypeWallet &&
                self.restorationResult != RestorationTableViewControllerResultBacked) {
                
                [self dismissNowOrSetPending];
            } else if (self.restorationResult == RestorationTableViewControllerResultBacked) {
                [self.identityClient requestStartPinlessRegistrationProcedure];
                self.restorationResult = RestorationTableViewControllerResultUnknown;
            }

            break;
        }
        case IdentityClientStateAwaitingUserPINChoice:
        {
            [self showFirstPINEntryScreenAsRoot:asRoot];
            break;
        }
        case IdentityClientStateAwaitingUserPINConfirmation:
        {
            if (asRoot) {
                [self showFirstPINEntryScreenAsRoot:asRoot];
            }
            break;
        }
        case IdentityClientStateCreatingIdentity:
        {
            if (!self.identityClient.reactivationInProgress) {
                [self.view endEditing:YES];
                overlayTitle = NSLocalizedString(@"Registrierung wird vorbereitet…", @"Registrierung");
            } else {
                overlayTitle = NSLocalizedString(@"Reaktivierung wird vorbereitet…", @"Registrierung");
            }
            break;
        }
        case IdentityClientStateAwaitingUserRegistration:
        {
            if (self.identityClient.identityClientType == IdentityClientTypeWallet) {
                [self showRegistrationFormAsRoot:asRoot];
            } else if (self.identityClient.identityClientType == IdentityClientTypeMerchant) {
                [self showActivationFormAsRoot:asRoot];
            }
            
            break;
        }
        case IdentityClientStateAwaitingInvitationCode:
        {
            [self showInvitationCodeFormAsRoot:asRoot];
            break;
        }
        case IdentityClientStateAcquiringRegistration:
        {
            if ([self.topViewController isKindOfClass:RegistrationFormViewController.class]) {
                [self.topViewController.view endEditing:YES];
            }
            overlayTitle = NSLocalizedString(@"Registrierung wird durchgeführt…", @"Registrierung");
            break;
        }
        case IdentityClientStateAwaitingActivation:
        {
            if (self.viewControllers.count < 2) {
                // load registration/activation form in hierarchy background, just in case user wants to restart
                if (self.identityClient.identityClientType == IdentityClientTypeWallet) {
                    [self showRegistrationFormAsRoot:YES];
                } else if (self.identityClient.identityClientType == IdentityClientTypeMerchant) {
                    for (UIViewController *viewController in self.viewControllers) {
                        if ([viewController isKindOfClass:ActivationNoteViewController.class]) {
                            [self showActivationNoteAsRoot:YES withState:ActivationNoteViewControllerStateWaiting];
                            return;
                        }
                    }
                    [self showRegistrationFormAsRoot:YES];
                }
            }
            [self showActivationNoteAsRoot:NO withState:ActivationNoteViewControllerStateWaiting];
            break;
        }
        case IdentityClientStateAquiringPingActivationResponse:
        {
            [self showActivationNoteAsRoot:NO withState:ActivationNoteViewControllerStateChecking];
            break;
        }
        case IdentityClientStateAwaitingUserPINProof:
        case IdentityClientStateBlockedWithWrongPIN:
        case IdentityClientStateAwaitingUserPINUnlock:
        case IdentityClientStateFailedUserPINProof:
        {
            self.hasPotentiallyCompletedPinChangeTask = NO;

            BackNavigationType backNavigationType;
            if (state == IdentityClientStateAwaitingUserPINProof || state == IdentityClientStateFailedUserPINProof) {
                backNavigationType = BackNavigationTypeCancel;
            } else {
                backNavigationType = BackNavigationTypeNone;
            }

            [self showPinValidationAsRoot:asRoot||state==IdentityClientStateAwaitingUserPINUnlock backNavigationType:backNavigationType];

            if (state == IdentityClientStateBlockedWithWrongPIN || state == IdentityClientStateFailedUserPINProof) {
                self.shouldShakeBecauseOfWrongPinEntry = YES;

                OverlayView *overlayView = [OverlayView instanceWithStyle:OverlayViewStyleBusyView text:NSLocalizedString(@"Falscher Code, bitte probiere es in einem Moment erneut.", @"Code Eingabe fehlerhaft") buttonStyle:OverlayViewButtonStyleNone buttonAction:nil];
                [[OverlayManager sharedInstance] showOverlayView:overlayView onViewController:self animated:YES completion:nil];
                
                [overlayView.countDownLabel setCountdownTimeInterval:self.identityClient.remainingTimeToAllowUnlock];
                overlayView.countDownLabel.hidden = NO;
            }

            break;
        }

        case IdentityClientStateFailedUserPINConfirmation:
        {
            @weakify(self);
            OverlayView *overlayView = [OverlayView instanceWithStyle:OverlayViewStylePlain text:NSLocalizedString(@"Die beiden Codes stimmen nicht überein, bitte Code erneut eingeben.", @"Code Eingabe fehlerhaft") buttonStyle:OverlayViewButtonStyleContinue buttonAction:^{
                @strongify(self);
                [self.identityClient retryChangeOfPIN];
            }];
            [[OverlayManager sharedInstance] showOverlayView:overlayView onViewController:self animated:YES completion:nil];
            
            break;
        }
        
        case IdentityClientStateAwaitingRestorationEmail: {
            [self showRestorationEmailFormViewControllerAsRoot:NO];
            break;
        }
        
        case IdentityClientStateSendingRestorationPukRequest: {
            [self showActivityIndicatorViewForRestorationEmailFormViewController];
            break;
        }
        
        case IdentityClientStateAwaitingRestorationPuk: {
            [self showRestorationPUKFormViewControllerAsRoot:NO];
            break;
        }
            
        case IdentityClientStateAwaitingRegistrationPuk: {
            [self showRegistrationPUKFormViewControllerAsRoot:NO];
            break;
        }
        
        case IdentityClientStateAcquiringReactivationMail:
        {
            overlayTitle = NSLocalizedString(@"Reaktivierungsmail wird angefordert...", @"Reaktivierung");
            break;
        }
        case IdentityClientStateAwaitingMailReactivationToken:
        {
            [self showReactivationNoteAsRoot:NO];
            break;
        }
        case IdentityClientStateAcquiringReactivationResponse:
        {
            overlayTitle = NSLocalizedString(@"Reaktivierung wird durchgeführt...", @"Reaktivierung");
            break;
        }
        case IdentityClientStateUnlocked:
        {
            self.pinValidationViewController = nil;
            [self showFinalOverlayForTaskThenComplete];
            return;
        }

        // unhandled cases, require no UI change
        
        case IdentityClientStateUninitialised:{
            DDLogWarn(@"%@: updating with uninitialized IdentityClient, nothing can be shown for this state. %@", self.class,  stringFromIdentityClientState(state));
            break;   
        }
        
        case IdentityClientStateAnalyzingAssets:break;
        case IdentityClientStateWaitingForForeground: break;
            
        case IdentityClientStateApplyingNewPIN:
        {
            self.hasPotentiallyCompletedPinChangeTask = YES;
            break;
        }

        case IdentityClientStateError:
        {
            [self showTerminalNoteAsRoot:YES];
            break;
        };
    }

    if (overlayTitle) {
        DDLogVerbose(@"%@: showing overlay text", NSStringFromClass(self.class));
        OverlayView *overlayView = [OverlayView instanceWithStyle:OverlayViewStyleBusyView text:overlayTitle buttonStyle:OverlayViewButtonStyleNone buttonAction:nil];
        [[OverlayManager sharedInstance] showOverlayView:overlayView onViewController:self animated:YES completion:nil];
    }
    
    [self setNeedsStatusBarAppearanceUpdate];
}

Usage of MerchantServiceClient

This client is responsible for doing merchant services like:

  • start client
  • stop client
  • fetch registered users
  • add users
  • remove users
  • fetch transactions
  • check if merchant has bonus program
  • check if merchant has gold program
  • check if merchant has coupon program
  • fetch bonus program details
  • handle identity client events

To start merchant service client(should be called right after instantiation of MerchantServiceClient) you can use method:

- (void)start;

To stop merchant service client you can use method:

- (void)stop;

To fetch registered users you can use method:

- (void)enqueueFetchRegisterUsersWithCompletion:(MerchantServiceClientCompletion)completion;

To add user(s) you can use method:

- (void)enqueueAddRegisterUserWithName:(NSString *)name completion:(MerchantServiceClientCompletion)completion;

To remove user(s) you can use method:

- (void)enqueueRemoveRegisterUserWithID:(KKW_ID)userId completion:(MerchantServiceClientCompletion)completion;

To check if merchant has bonus program you can use property:

@property (nonatomic, readonly) BOOL hasBonusProgram;

To check if merchant has gold program you can use property:

@property (nonatomic, readonly) BOOL hasGoldProgram;

To check if merchant has coupon program you can use property:

@property (nonatomic, readonly) BOOL hasCouponProgram;

To fetch bonus program details you can use next method:

- (void)updateBonusProgramDetails;

and then to access bonus program details you can use next property:

@property (nonatomic, readonly) MerchantBonusProgramDetailsVehicle *bonusProgramDetailsVehicle;

To listen on IdentityClient events you can use methods:

- (void)handleIdentityClientDidGetReady;
- (void)handleIdentityClientDidInvalidateIdentity;

Next very important object is PaymentHost object.

PaymentHost

In order to instantiate PaymentHost object we need IdentityClient and BluetoothConflictObserver objects to be instantiated first.

Above it's already shown how to instantiate IdentityClient for merchant part and here is the code how to instantiate BluetoothConflictObserver object.

@property (nonatomic, strong) id <BluetoothConflictObserver> conflictObserver;
self.conflictObserver = (id<BluetoothConflictObserver>) [BluetoothAvailabilityClient clientForPaymentAndRedemption];

BluetoothConflictObserver is responsible for like the name says for confilct evaluation, observing conflict state and conflict count

- (BluetoothConflictObserverState)conflictObserverState;  // no KVO guaranteed here
- (NSInteger)conflictCount; // no KVO guaranteed here

Besides that it can be used for restart scanning and stop scanning:

- (void)restart;    // restarts scanning
- (void)stop;       // stops scanning

We can also access to bluetooth state here:

@property (nonatomic, readonly) BluetoothAvailabilityClientState state;

Back to PaymentHost. Now we have everything we need to instantiate this object:

 self.paymentHost = [PaymentHost instanceWithIdentityClient:self.identityClient conflictObserver:self.conflictObserver];

Usage of PaymentHost

The main purpose of PaymentHost object is to: start the payment, stop the payment and cancel the payment.

- (BOOL)startWithAmount:(long)amount registerUser:(RegisterUser *)registerUser;
- (BOOL)stop;
- (void)cancel; // cancel a payment

In order to start payment merchant need to enter amount and call this method:

[self.paymentHost startWithAmount:self.amount registerUser:self.merchantServiceClient.selectedRegisterUserVehicle.result];

and to observe on PaymentHost state:

 [self.KVOControllerNonRetaining observe:self keyPath:@keypath(self.paymentHost.state) options:NSKeyValueObservingOptionNew action:@selector(update)];

- (void)update
{
    PaymentOrder *paymentOrder = self.paymentHost.finalPaymentState.paymentOrder;
    TerminalPaymentViewModel *viewModel = [[TerminalPaymentViewModel alloc] initWithPaymentHostState:self.paymentHost.state
                                                                                   previousViewModel:self.viewModel
                                                                                              amount:self.amount
                                                                                                 ccy:paymentOrder.currency
                                                                                                 tip:paymentOrder.tip
                                                                                 userDecisionTimeout:self.paymentHost.userDecisionTimeout];
    
    // Completed payment
    if (viewModel.payingProgressViewState == TerminalPaymentProgressViewStateDone) {
        [self invalidatePaymentTimer];
        [self performSelector:@selector(close) withObject:self afterDelay:3.0];
    }
    
    [self updateFromViewModel:self.viewModel toViewModel:viewModel];
    
    self.viewModel = viewModel;
    
    if (self.paymentHost.state == PaymentHostStateCompletedWithError) {
        if (self.paymentHost.isCancelled) {
            [self.paymentHost stop];
            [self invalidatePaymentTimer];
            self.completion(TerminalPaymentViewControllerResultCancelled);
        } else {
            [Alert presentModalMessage:[MessageModel messageModelForError:self.paymentHost.humanizedError] onViewController:self tapBlock:^{
                [self.paymentHost stop];
                [self invalidatePaymentTimer];
                self.completion(TerminalPaymentViewControllerResultFailed);
            }];
        }
    }
}

When user leave this screen we need to stop observing and to stop PaymentHost client.

[self stopObserving];
[self.paymentHost stop];

- (void)stopObserving
{
    [self.KVOControllerNonRetaining unobserve:self];
}

Next important object is RedemptionHostobject. Here we have:

  • BonusRedemptionHost
  • GoldRedemptionHost
  • CouponRedemptionHost

In order to instantiate all of them we can use next code:

@property (nonatomic, strong, readwrite) RedemptionHost *bonusRedemptionHost;
@property (nonatomic, strong, readwrite) RedemptionHost *goldRedemptionHost;
@property (nonatomic, strong, readwrite) RedemptionHost *couponRedemptionHost;

self.bonusRedemptionHost = [RedemptionHost redemptionHostForTransactionTypeWithRequestManager:self.identityClient.requestManager bluetoothConflictObserver:self.conflictObserver transactionType:TransactionTypeBonusRedemption];
self.goldRedemptionHost = [RedemptionHost redemptionHostForTransactionTypeWithRequestManager:self.identityClient.requestManager bluetoothConflictObserver:self.conflictObserver transactionType:TransactionTypeGoldRedemption];
self.couponRedemptionHost = [RedemptionHost redemptionHostForTransactionTypeWithRequestManager:self.identityClient.requestManager bluetoothConflictObserver:self.conflictObserver transactionType:TransactionTypeCouponRedemption];

Next we need to instantiate RequestProxyHostCoordinator

RequestProxyHostCoordinator

@property (nonatomic, strong) RequestProxyHostCoordinator *requestProxyHostCoordinator;

self.requestProxyHostCoordinator = [[RequestProxyHostCoordinator alloc] initWithProxyHostConnectors:@[
        (id<ProxyHostConnector>)self.paymentHost.connector,
        (id<ProxyHostConnector>)self.bonusRedemptionHost.connector,
        (id<ProxyHostConnector>)self.goldRedemptionHost.connector,
        (id<ProxyHostConnector>)self.couponRedemptionHost.connector
]];

[self.requestProxyHostCoordinator configure];

At the end we need to start to observe few things:

- (void)startObserving
{
    [self startObservingApplication];
    [self startObservingIdentityClient];
}

Observing Application:

- (void)startObservingApplication
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleApplicationWillEnterForegroundNotification2:) name:ApplicationWillEnterForegroundNotification(2) object:nil];
}

- (void)handleApplicationWillEnterForegroundNotification2:(NSNotification *)notification
{
    [self.touchIDSupport handleApplicationWillEnterForeground];
}

Observing IdentityClient

- (void)startObservingIdentityClient
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIdentityClientDidInvalidateIdentityNotification:) name:IdentityClientDidInvalidateIdentityNotification object:self.identityClient];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIdentityClientIdentityDidGetReadyNotification:) name:IdentityClientIdentityDidGetReadyNotification object:nil];
}

- (void)handleIdentityClientDidInvalidateIdentityNotification:(NSNotification *)notification
{
    [self.merchantServiceClient handleIdentityClientDidInvalidateIdentity];
}

- (void)handleIdentityClientIdentityDidGetReadyNotification:(NSNotification *)notification
{
    [self.merchantServiceClient handleIdentityClientDidGetReady];
}