From 1001c93171f5236d592962d9cf1928c9cba2d799 Mon Sep 17 00:00:00 2001 From: sogapps Date: Wed, 25 Mar 2026 13:45:36 -0400 Subject: [PATCH] feat(flutterwave): UI cleanup (Lottie v4, nav/title fixes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update LoadingHUD.swift to support Lottie v4.6.0 (LottieAnimationView, modern API, cleanup) - Remove artificial section spacing between “Debit Card” and “USSD” in FlutterwavePayViewController - Replace close button with system "xmark" (SF Symbols) - Refactor FlutterwavePayNavTitle: • Replace button with UILabel + attributed icon (prevents truncation) • Use NSTextAttachment for padlock + “FLW SECURED” • Pin label to edges + enable dynamic text scaling • Remove deprecated inset usage • Disable interaction (informational only) - General code cleanup and modernization --- .../contents.xcworkspacedata | 7 + .../UI/FlutterwavePayViewController.swift | 82 ++++++---- .../BaseViewController.swift | 4 +- .../FlutterWave+UIPickerView.swift | 6 +- FlutterwaveSDK/Classes/Utils/LoadingHUD.swift | 148 +++++++++--------- 5 files changed, 133 insertions(+), 114 deletions(-) create mode 100644 Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Example/Pods/Pods.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/FlutterwaveSDK/Classes/UI/FlutterwavePayViewController.swift b/FlutterwaveSDK/Classes/UI/FlutterwavePayViewController.swift index 1b7b279..aabb27f 100644 --- a/FlutterwaveSDK/Classes/UI/FlutterwavePayViewController.swift +++ b/FlutterwaveSDK/Classes/UI/FlutterwavePayViewController.swift @@ -620,7 +620,7 @@ public class FlutterwavePayViewController: BaseViewController { let closeButton = UIButton(type: .system) closeButton.tintColor = .darkGray - closeButton.setImage(UIImage(named: "rave_close", in: Bundle.getResourcesBundle(), compatibleWith: nil), for: .normal) + closeButton.setImage(UIImage(systemName: "xmark"), for: .normal) closeButton.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .bold) closeButton.titleLabel?.textAlignment = .center closeButton.frame = CGRect(x: 0, y:0, width: 40, height: 40) @@ -629,6 +629,9 @@ public class FlutterwavePayViewController: BaseViewController { self.tableView.backgroundColor = UIColor(hex: "#F2F2F2") self.tableView.tableFooterView = UIView(frame: .zero) + self.tableView.estimatedSectionHeaderHeight = 0 + self.tableView.estimatedSectionFooterHeight = 0 + self.tableView.estimatedRowHeight = 0 configureView() configureDebitCardView() configureGBPView() @@ -710,7 +713,7 @@ public class FlutterwavePayViewController: BaseViewController { //MARK CVV texfield button action self.debitCardView.questionButton.rx.tap.subscribe(onNext: { - // self.showToast(message: "Your Toast Message") + // self.showFWToast(message: "Your Toast Message") }).disposed(by: disposableBag) @@ -971,7 +974,7 @@ public class FlutterwavePayViewController: BaseViewController { @objc func chargeGBPAccountFlow(){ self.view.endEditing(true) if FlutterwaveConfig.sharedConfig().currencyCode == .some("GBP"){ - LoadingHUD.shared().show() + LoadingHUD.shared.show() flutterwaveUKAccountClient.amount = self.amount flutterwaveUKAccountClient.accountNumber = "00000" flutterwaveUKAccountClient.bankCode = "093" @@ -985,7 +988,7 @@ public class FlutterwavePayViewController: BaseViewController { @objc func completeGBPAccountFlow(){ self.view.endEditing(true) if FlutterwaveConfig.sharedConfig().currencyCode == .some("GBP"){ - LoadingHUD.shared().show() + LoadingHUD.shared.show() // raveUKAccountClient.queryTransaction(txRef: raveUKAccountClient.txRef) } } @@ -1155,7 +1158,7 @@ public class FlutterwavePayViewController: BaseViewController { func chargeUSAccountFlow(){ self.view.endEditing(true) if FlutterwaveConfig.sharedConfig().country == .some("US"){ - LoadingHUD.shared().show() + LoadingHUD.shared.show() flutterwaveAccountClient.isUSBankAccount = true flutterwaveAccountClient.amount = self.amount flutterwaveAccountClient.phoneNumber = FlutterwaveConfig.sharedConfig().phoneNumber @@ -1167,7 +1170,7 @@ public class FlutterwavePayViewController: BaseViewController { func chargeSAAccountFlow(){ self.view.endEditing(true) if FlutterwaveConfig.sharedConfig().country == .some("ZA"){ - LoadingHUD.shared().show() + LoadingHUD.shared.show() flutterwaveAccountClient.amount = self.amount flutterwaveAccountClient.accountNumber = "00000" flutterwaveAccountClient.bankCode = "093" @@ -1295,6 +1298,9 @@ public class FlutterwavePayViewController: BaseViewController { } } override public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + // If this section's header height is 0, return nil explicitly + let headerHeight = self.tableView(tableView, heightForHeaderInSection: section) + guard headerHeight > 0 else { return nil } switch section { case 0: @@ -1458,6 +1464,18 @@ public class FlutterwavePayViewController: BaseViewController { } + override public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + // 0 for hidden sections; use leastNormalMagnitude or a small value for visible ones if needed + let headerHeight = self.tableView(tableView, heightForHeaderInSection: section) + return headerHeight == 0 ? 0 : .leastNormalMagnitude + } + + override public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + // Return nil for hidden sections + let headerHeight = self.tableView(tableView, heightForHeaderInSection: section) + return headerHeight == 0 ? nil : UIView(frame: .zero) + } + func getHeader()-> FlutterwaveHeaderView{ let header = FlutterwaveHeaderView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 100)) header.backgroundColor = UIColor(hex: "#FBEED8") @@ -1531,7 +1549,7 @@ public class FlutterwavePayViewController: BaseViewController { func saveCardCallbacks(){ if let _ = FlutterwaveConfig.sharedConfig().publicKey{ if let deviceNumber = FlutterwaveConfig.sharedConfig().phoneNumber, deviceNumber != ""{ - // LoadingHUD.shared().show() + // LoadingHUD.shared.show() CardViewModel.sharedViewModel.fetchCard() } } @@ -1550,14 +1568,14 @@ public class FlutterwavePayViewController: BaseViewController { // return // } // DispatchQueue.main.async { -// LoadingHUD.shared().hide() +// LoadingHUD.shared.hide() // strongSelf.showOTP(message: message ?? "Enter the OTP sent to your mobile phone and email address to continue", flwRef: "", otpType: .savedCard) // } // } // flutterwaveCardClient.sendOTPError = {(message) in // // DispatchQueue.main.async { -// LoadingHUD.shared().hide() +// LoadingHUD.shared.hide() // showSnackBarWithMessage(msg: message ?? "An error occured while sending OTP") // } // } @@ -1584,7 +1602,7 @@ public class FlutterwavePayViewController: BaseViewController { return } DispatchQueue.main.async { - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() strongSelf.debitCardView.isHidden = false strongSelf.saveCardContainer.isHidden = true } @@ -1595,7 +1613,7 @@ public class FlutterwavePayViewController: BaseViewController { // return // } // DispatchQueue.main.async { -// LoadingHUD.shared().hide() +// LoadingHUD.shared.hide() // if let count = saveCards?.count, count > 0 { // strongSelf.saveCardContainer.isHidden = false // strongSelf.debitCardView.isHidden = true @@ -1611,7 +1629,7 @@ public class FlutterwavePayViewController: BaseViewController { // return // } // DispatchQueue.main.async { -// LoadingHUD.shared().hide() +// LoadingHUD.shared.hide() // strongSelf.debitCardView.isHidden = false // strongSelf.saveCardContainer.isHidden = true // } @@ -1629,27 +1647,27 @@ public class FlutterwavePayViewController: BaseViewController { } flutterwaveUKAccountClient.chargeSuccess = {[weak self](flwRef,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.delegate?.tranasctionSuccessful(flwRef: flwRef,responseData: data) self?.dismiss(animated: true) } flutterwaveUKAccountClient.chargeGBPOTPAuth = {[weak self](flwRef, reference,message) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.showGBPBankDetails(reference) } flutterwaveUKAccountClient.redoChargeOTPAuth = {[weak self](flwRef, message) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.showOTP(message: message, flwRef: flwRef, otpType: .bank) } flutterwaveUKAccountClient.chargeWebAuth = {[weak self](flwRef, authURL) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.showWebView(url: authURL,ref:flwRef) } flutterwaveUKAccountClient.validateError = {[weak self](message,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() //Still show success if let _data = data{ let flwref = _data.flwRef @@ -1664,7 +1682,7 @@ public class FlutterwavePayViewController: BaseViewController { self?.dismiss(animated: true) } flutterwaveUKAccountClient.error = {(message,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() DispatchQueue.main.async { if let msg = message{ if msg.containsIgnoringCase(find: "Timed Out"){ @@ -1727,27 +1745,27 @@ public class FlutterwavePayViewController: BaseViewController { } flutterwaveAccountClient.chargeSuccess = {[weak self](flwRef,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.delegate?.tranasctionSuccessful(flwRef: flwRef,responseData: data) self?.dismiss(animated: true) } flutterwaveAccountClient.chargeOTPAuth = {[weak self](flwRef, message) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.showOTP(message: message, flwRef: flwRef, otpType: .bank) } flutterwaveAccountClient.redoChargeOTPAuth = {[weak self](flwRef, message) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.showOTP(message: message, flwRef: flwRef, otpType: .bank) } flutterwaveAccountClient.chargeWebAuth = {[weak self](flwRef, authURL) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.showWebView(url: authURL,ref:flwRef) } flutterwaveAccountClient.validateError = {[weak self](message,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() //Still show success if let _data = data{ @@ -1763,7 +1781,7 @@ public class FlutterwavePayViewController: BaseViewController { self?.dismiss(animated: true) } flutterwaveAccountClient.error = {(message,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() DispatchQueue.main.async { if let msg = message{ if msg.containsIgnoringCase(find: "Timed Out"){ @@ -1781,7 +1799,7 @@ public class FlutterwavePayViewController: BaseViewController { flutterwaveCardClient.transactionReference = FlutterwaveConfig.sharedConfig().transcationRef flutterwaveCardClient.chargeSuccess = {[weak self](flwRef,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.delegate?.tranasctionSuccessful(flwRef: flwRef,responseData: data) self?.dismiss(animated: true) } @@ -1794,7 +1812,7 @@ public class FlutterwavePayViewController: BaseViewController { } flutterwaveCardClient.chargeSuggestedAuth = {[weak self](authModel, data, authURL) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() switch authModel { case .AVS_VBVSECURECODE: self?.showBillingAddress() @@ -1817,7 +1835,7 @@ public class FlutterwavePayViewController: BaseViewController { } flutterwaveCardClient.error = {(message,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() DispatchQueue.main.async { if let msg = message{ if msg.containsIgnoringCase(find: "timeout"){ @@ -1830,16 +1848,16 @@ public class FlutterwavePayViewController: BaseViewController { } flutterwaveCardClient.chargeOTPAuth = {[weak self](flwRef, message) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.showOTP(message: message, flwRef: flwRef, otpType: .card) } flutterwaveCardClient.chargeWebAuth = {[weak self](flwRef, authURL) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() self?.showWebView(url: authURL,ref:flwRef) } flutterwaveCardClient.validateError = {[weak self](message,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() if let _data = data{ let flwref = _data.flwRef.orEmpty() self?.delegate?.tranasctionSuccessful(flwRef: flwref,responseData: data) @@ -1866,7 +1884,7 @@ public class FlutterwavePayViewController: BaseViewController { flutterwaveMpesaClient.chargePending = {[weak self] (title,message) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() DispatchQueue.main.async { guard let strongSelf = self else{ return} self?.showMpesaPending(mesage: message ?? "") @@ -1879,7 +1897,7 @@ public class FlutterwavePayViewController: BaseViewController { self?.dismiss(animated: true) } flutterwaveMpesaClient.error = {[weak self](message,data) in - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() DispatchQueue.main.async { if let msg = message{ if msg.containsIgnoringCase(find: "timeout"){ diff --git a/FlutterwaveSDK/Classes/UI/NewExtraFlutterwave/BaseViewController.swift b/FlutterwaveSDK/Classes/UI/NewExtraFlutterwave/BaseViewController.swift index cee7c85..932dd00 100644 --- a/FlutterwaveSDK/Classes/UI/NewExtraFlutterwave/BaseViewController.swift +++ b/FlutterwaveSDK/Classes/UI/NewExtraFlutterwave/BaseViewController.swift @@ -43,9 +43,9 @@ public class BaseViewController:UITableViewController{ baseViewModel.isLoading.subscribe(onNext: { isLoading in DispatchQueue.main.async { if(isLoading){ - LoadingHUD.shared().show() + LoadingHUD.shared.show() }else{ - LoadingHUD.shared().hide() + LoadingHUD.shared.hide() } } diff --git a/FlutterwaveSDK/Classes/UI/NewExtraFlutterwave/FlutterWave+UIPickerView.swift b/FlutterwaveSDK/Classes/UI/NewExtraFlutterwave/FlutterWave+UIPickerView.swift index 462dde0..cf8bd2b 100644 --- a/FlutterwaveSDK/Classes/UI/NewExtraFlutterwave/FlutterWave+UIPickerView.swift +++ b/FlutterwaveSDK/Classes/UI/NewExtraFlutterwave/FlutterWave+UIPickerView.swift @@ -17,7 +17,7 @@ extension FlutterwavePayViewController : UITextFieldDelegate,CardSelect,UIPicker func cardSelected(card: SavedCard?) { flutterwaveCardClient.selectedCard = card if let card = flutterwaveCardClient.selectedCard{ - //LoadingHUD.shared().show() + //LoadingHUD.shared.show() CardViewModel.sharedViewModel.sendCardOtp(cardHash: card.cardHash ?? "") // flutterwaveCardClient.sendOTP(card: card) } @@ -296,7 +296,7 @@ extension FlutterwavePayViewController : UITextFieldDelegate,CardSelect,UIPicker guard let otp = accountOtpContentContainer.otpTextField.text, otp != "" else { return } - LoadingHUD.shared().show() + LoadingHUD.shared.show() flutterwaveAccountClient.otp = otp // raveAccountClient.validateAccountOTP() } @@ -319,7 +319,7 @@ extension FlutterwavePayViewController : UITextFieldDelegate,CardSelect,UIPicker guard let otp = otpContentContainer.otpTextField.text, otp != "" else { return } - // LoadingHUD.shared().show() + // LoadingHUD.shared.show() flutterwaveCardClient.otp = otp flutterwaveCardClient.isSaveCardCharge = "1" flutterwaveCardClient.saveCardPayment = "saved-card" diff --git a/FlutterwaveSDK/Classes/Utils/LoadingHUD.swift b/FlutterwaveSDK/Classes/Utils/LoadingHUD.swift index 8f2cee8..a960e6a 100644 --- a/FlutterwaveSDK/Classes/Utils/LoadingHUD.swift +++ b/FlutterwaveSDK/Classes/Utils/LoadingHUD.swift @@ -3,109 +3,103 @@ // GetBarter // // Created by Olusegun Solaja on 04/08/2018. -// Copyright © 2018 Olusegun Solaja. All rights reserved. +// Updated by SOGApps on 03/24/2026. +// Updated for Lottie v4.6.0 // import UIKit import Lottie class LoadingHUD: UIView { - //let appDelegate = UIApplication.shared.delegate as? AppDelegate - var animation:AnimationView! - - var bgColor: UIColor? = .clear - var applyBlur = true - var animationFile = "Loader_YW" - - var blurView:UIVisualEffectView = { - let effect = UIBlurEffect(style: UIBlurEffect.Style.dark) + + // MARK: - Properties + static let shared = LoadingHUD(frame: UIScreen.main.bounds) + + private var animation: LottieAnimationView? + var animationFile: String = "Loader_YW" + var bgColor: UIColor = .clear + var applyBlur: Bool = true + + private lazy var blurView: UIVisualEffectView = { + let effect = UIBlurEffect(style: .dark) let effectView = UIVisualEffectView(effect: effect) effectView.translatesAutoresizingMaskIntoConstraints = false return effectView }() - - - class func shared() -> LoadingHUD{ - struct Static { - static let loader = LoadingHUD(frame: (UIScreen.main.bounds)) - - } - return Static.loader - } - - - + + // MARK: - Initializers override init(frame: CGRect) { super.init(frame: frame) + self.isHidden = true setupUI() } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) + + required init?(coder: NSCoder) { + super.init(coder: coder) + self.isHidden = true setupUI() } - - func setupUI(){ - - } - - func show(){ + + private func setupUI() { backgroundColor = bgColor - if(applyBlur){ - insertSubview(blurView, at: 0) - blurView.leftAnchor.constraint(equalTo:leftAnchor).isActive = true - blurView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true - blurView.topAnchor.constraint(equalTo: topAnchor).isActive = true - blurView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true - } - - - animation = AnimationView(name: animationFile, bundle: Bundle.getResourcesBundle() ?? Bundle.main) - animation.loopMode = .loop - animation.translatesAutoresizingMaskIntoConstraints = false - addSubview(animation) - animation.centerXAnchor.constraint(equalTo:centerXAnchor).isActive = true - animation.centerYAnchor.constraint(equalTo:centerYAnchor).isActive = true - animation.widthAnchor.constraint(equalToConstant: 80).isActive = true - animation.heightAnchor.constraint(equalToConstant: 80).isActive = true - - self.animation.play() - UIApplication.shared.keyWindow?.addSubview(self) - isHidden = false } - func showInView(view:UIView){ + + // MARK: - Show HUD + func show() { + guard let keyWindow = UIApplication.shared.connectedScenes + .compactMap({ $0 as? UIWindowScene }) + .flatMap({ $0.windows }) + .first(where: { $0.isKeyWindow }) else { return } + + showInView(view: keyWindow) + } + + func showInView(view: UIView) { backgroundColor = bgColor - if(applyBlur){ - insertSubview(blurView, at: 0) - blurView.leftAnchor.constraint(equalTo:leftAnchor).isActive = true - blurView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true - blurView.topAnchor.constraint(equalTo: topAnchor).isActive = true - blurView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + + if applyBlur { + if blurView.superview == nil { + insertSubview(blurView, at: 0) + NSLayoutConstraint.activate([ + blurView.leadingAnchor.constraint(equalTo: leadingAnchor), + blurView.trailingAnchor.constraint(equalTo: trailingAnchor), + blurView.topAnchor.constraint(equalTo: topAnchor), + blurView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) + } } - - animation = AnimationView(name: animationFile) + + // Remove previous animation if any + animation?.removeFromSuperview() + + // Load animation + let lottieAnimation = LottieAnimation.named(animationFile, bundle: Bundle.getResourcesBundle() ?? Bundle.main) + animation = LottieAnimationView(animation: lottieAnimation) + guard let animation = animation else { return } animation.loopMode = .loop animation.translatesAutoresizingMaskIntoConstraints = false addSubview(animation) - animation.centerXAnchor.constraint(equalTo:centerXAnchor).isActive = true - animation.centerYAnchor.constraint(equalTo:centerYAnchor).isActive = true - animation.widthAnchor.constraint(equalToConstant: 80).isActive = true - animation.heightAnchor.constraint(equalToConstant: 80).isActive = true - - self.animation.play() - view.addSubview(self) - + + NSLayoutConstraint.activate([ + animation.centerXAnchor.constraint(equalTo: centerXAnchor), + animation.centerYAnchor.constraint(equalTo: centerYAnchor), + animation.widthAnchor.constraint(equalToConstant: 80), + animation.heightAnchor.constraint(equalToConstant: 80) + ]) + + animation.play() + + if superview == nil { + view.addSubview(self) + } isHidden = false - } - - func hide(){ -// animation.stop() + + // MARK: - Hide HUD + func hide() { + animation?.stop() + animation?.removeFromSuperview() isHidden = true removeFromSuperview() } - - } - -