iOS SDK UI Customization API

UI and Key Mapping Reference

This article describes two different ways to customize your iOS application using the iOS SDK:

  • Static Resources customize the UI layout during build time.
  • Programmatic Customization modifies the layout at run time.

🔖

Note:

You can combine static and programmatic approaches for customization.

To use the SDK in the most flexible way, combine the reference in this article with the iOS SDK.

Sample Application: Refer to the iOS sample application for example code.

Layouts

How to Use the Layout Images and Index Tables

This section includes screenshots of layouts for the SDK functionality. As you review the layouts, you can cross-reference the alphanumeric tags in the images with the values in the index tables that follow this section:

New PIN Code

Confirm PIN Code

Enter PIN Code

Security Intros

Security Questions

Select Question

Security Summary

Security Confirm

Recover PIN Code

Index Table A

Based on the layouts, table A provides the UI item named in the controller and the keys currently in use. You can get the UIViewController from WalletSdkDelegate and customize the UI items in the run time. Also, you can provide the values in static resources CirclePWLocalizable.strings and CirclePWTheme.json for customization layouts in build time.

 Programmatic CustomizationStatic Resources
    CirclePWLocalizable.stringsCirclePWTheme.json
#ControllerItemNote fontcolor
A1NewPINCodeViewControllertitleLabel1-circlepw_new_pincode_headlinesemiboldtext_main
A2titleLabel2-circlepw_new_pincode_headline_2boldtitle_gradients
A3subtitleLabel-circlepw_new_pincode_subheadregulartext_auxiliary
A4BasePINInputViewController-dots and texts will be generated dynamically-lightpin_dot_base / pin_dot_base_border / pin_dot_activated / success / error
A5-shows ApiError.displayString-regularerror
A6showPINButtonshow pincirclepw_show_pinsemiboldtext_action / text_action_pressed
A7hide pincirclepw_hide_pin
A8ConfirmPINCodeViewControllertitleLabel1-circlepw_confirm_pincode_headlinesemiboldtext_main
A9titleLabel2-circlepw_confirm_pincode_headline_2boldtitle_gradients
A10subtitleLabel-circlepw_confirm_pincode_subheadregulartext_auxiliary
A11EnterPINCodeViewControllertitleLabel1-circlepw_enter_pincode_headlinesemiboldtext_main
A12titleLabel2-circlepw_enter_pincode_headline_2boldtitle_gradients
A13subtitleLabel-circlepw_enter_pincode_subheadregulartext_auxiliary
A14forgotPINButton-circlepw_enter_pincode_forgot_pinsemiboldtext_action / text_action_pressed
A15SecurityIntrosViewControllertitleLabel1-circlepw_security_intros_headlinesemiboldtext_main
A16titleLabel2-circlepw_security_intros_headline_2boldtitle_gradients
A17introDescLabel-circlepw_security_intros_descriptionregulartext_auxiliary2
A18introLinkButton-circlepw_security_intros_linkregulartext_action / text_action_pressed
A18-1introLinkcustom your url string---
A19continueButtontextcirclepw_continuesemiboldmain_bt_text / main_bt_text_pressed / main_bt_text_disabled
background--main_bt_background / main_bt_background_pressed / main_bt_background_disabled
A20SecurityQuestionsViewControllerbaseNaviTitleLabel-circlepw_security_questions_titlemediumtext_main
A21nextButtontextcirclepw_nextsemiboldmain_bt_text / main_bt_text_pressed / main_bt_text_disabled
background--main_bt_background / main_bt_background_pressed / main_bt_background_disabled
A22SecurityQuestionTableViewCellquestionTitleLabel-circlepw_security_questions_question_headerregulartext_auxiliary
A23questionMarkLabel-circlepw_security_questions_required_markregularerror
A24-placeholdercirclepw_security_questions_question_placeholderregulartext_placeholder
selected question-regulartext_main
A25answerTitleLabel-circlepw_security_questions_answer_headerregulartext_auxiliary
A26-placeholdercirclepw_security_questions_answer_placeholderregulartext_placeholder
input text-regulartext_main
view--input_background_disabled / input_border / input_border_focused
A27hintTitleLabel-circlepw_security_questions_answer_hint_headerregulartext_auxiliary
A28-placeholdercirclepw_security_questions_answer_hint_placeholderregulartext_placeholder
input text-regulartext_main
view--input_background_disabled / input_border / input_border_focused
A29hintWarningLabelApiError(.hintsMatchAnswers)-regularerror
A30SelectQuestionViewControllerbaseNaviTitleLabel-circlepw_select_question_titlemediumtext_main
A31SelectQuestionTableViewCelltitleLabelquestion list-regulartext_main
A32SecuritySummaryViewControllerbaseNaviTitleLabel-circlepw_security_summary_titlemediumtext_main
A33continueButtontextcirclepw_continuesemiboldmain_bt_text / main_bt_text_pressed / main_bt_text_disabled
background--main_bt_background / main_bt_background_pressed / main_bt_background_disabled
A34SecuritySummaryTableViewCelltitleLabel-{ordinal} + circlepw_questionsemiboldtext_main2
A35questionTitleLabel-circlepw_question + “:”regulartext_auxiliary2
A36---regulartext_summary
A37answerTitleLabel-circlepw_answer + “:“regulartext_auxiliary2
A38---semiboldtext_summary_highlight
A39hintTitleLabel-circlepw_hint + “:“regulartext_auxiliary2
A40--circlepw_empty_placeholderregulartext_summary
A41SecurityConfirmViewControllerbaseNaviTitleLabel-circlepw_security_confirm_titlemediumtext_main
A42imageBgView---security_confirm_main_bg
A43tipsTitleLabel-circlepw_security_confirm_headlinemediumtext_main
A44agreeTitleLabel-circlepw_security_confirm_input_headlinesemiboldtext_main2
A45agreeTextFieldplaceholdercirclepw_security_confirm_input_placeholderregulartext_placeholder
input text-regulartext_main2
view--input_background_disabled / input_border / input_border_focused
A46continueButtontextcirclepw_continuesemiboldmain_bt_text / main_bt_text_pressed / main_bt_text_disabled
background--main_bt_background / main_bt_background_pressed / main_bt_background_disabled
A47RecoverPINCodeViewControllertitleLabel1-circlepw_recover_pincode_headlinesemiboldtext_main
A48titleLabel2-circlepw_recover_pincode_headline_2boldtitle_gradients
A49-shows ApiError.displayString-regulartext_main
A50errorMessageViewbackground--error_background
A51confirmButtontextcirclepw_confirmsemiboldmain_bt_text / main_bt_text_pressed / main_bt_text_disabled
background--main_bt_background / main_bt_background_pressed / main_bt_background_disabled
A52RecoverPINCodeTableViewCell---regulartext_main2
A53hintTitleLabeltextcirclepw_hintregularrecover_pin_hint_title
background--recover_pin_hint_title_bg
A54-placeholdercirclepw_empty_placeholderregularrecover_pin_hint
A55answerTitleLabel-circlepw_recover_pincode_answer_input_headerregulartext_auxiliary
A56answerMarkLabel-circlepw_security_questions_required_markregularerror
A57-placeholdercirclepw_recover_pincode_answer_input_placeholderregulartext_placeholder
input text-regulartext_main
view--input_background_disabled / input_border / input_border_focused

Index Table B

By confirming the WalletSdkLayoutProvider, you can custom layout from Table-B dynamically. See the Sample Code.

#WalletSdkLayoutProviderNote
B1func securityQuestions() -> [SecurityQuestion]Set security question list
B2func securityQuestionsRequiredCount() -> IntSet security question required count (default is 2)
B3func securityConfirmItems() -> [SecurityConfirmItem]Set security confirm item list
B4func displayDateFormat() -> StringSet the date format for display date strings. (Default is "yyyy-MM-dd")
-func imageStore() -> ImageStoreSet local and remote images (more details in Table-C)
-func themeFont() -> ThemeConfig.ThemeFont?Set theme font programmatically- Provide the ThemeFont structure by code. - This method will override the font setups in CirclePWTheme.json file. -

Index Table C

 Used in WalletSdkLayoutProviderUsed in WalletSdkDelegate
#ImageStore.Img (Enum)ControllerItem
C1naviClose--
C2naviBack--
C3securityIntroMainSecurityIntrosViewControllerintroImageView
C4dropdownArrowSecurityQuestionTableViewCellquestionTrailingButton
C5selectCheckMarkSelectQuestionTableViewCellcheckmarkImage
C6securityConfirmMainSecurityConfirmViewControllerimageView
C7errorInfoRecoverPINCodeViewControllererrorImageView
  • Use the ImageStore.Img (Enum) column to customize with the WalletSdkLayoutProvider:
func imageStore() -> ImageStore {
    let local: [ImageStore.Img: UIImage] = [
        .naviBack: UIImage(named: "ic_navi_back")!,
        .naviClose: UIImage(named: "ic_navi_close")!,
        .selectCheckMark: UIImage(named: "ic_checkmark")!,
        .dropdownArrow: UIImage(named: "ic_trailing_down")!,
        .errorInfo: UIImage(named: "ic_warning_alt")!,
        .securityIntroMain: UIImage(named: "img_security_intro")!,
        .securityConfirmMain: UIImage(named: "img_driver_blog")!
    ]

    let remote: [ImageStore.Img: URL] = [
        .securityIntroMain: URL(string: "https://www.circle.com/hs-fs/hubfs/Sundaes/810/global-payments-810x810.png")!,
        .securityConfirmMain: URL(string: "https://www.circle.com/hs-fs/hubfs/Sundaes/810/Trust-810x810.png")!,
    ]

    return ImageStore(local: local, remote: remote)
}
  • If you set both local and remote images, the remote image will present after the image loads, and the local image will be set as the placeholder image.
  • The remote image formats supported include those from the Apple system (JPEG, PNG, TIFF, BMP, etc.), GIF, and APNG animated images.
  • The remote image formats unsupported for new image formats include HEIC, BPG, AVIF, and vector formats such as PDF and SVG.
  • The Controller and Item column shows where the images are used.
    Refer to the UI items If you have special customizations with WalletSdkDelegate (not recommended)

Static Resources

This section describes how to customize the UI for iOS statically.

🔖

Note:

Circle recommends customizing the UI using static files.

You can copy the icon images from the Sample Project.

Assets

Because the CircleProgrammableWalletSDK does not contain any image resources for the SDK; you must provide the icon images from either local assets or remote URLs.

🚧

Important:

Setting the WalletSdkLayoutProvider.imageStore in WalletSdkLayoutProvider is required for you to see icons in the layouts.

CirclePWLocalizable.strings

Setup

  1. Create a file CirclePWLocalizable.strings in your main bundle.
  2. Provide your strings. Partial override is supported.

🔖

Note:

You can provide specific keys and values you want to override and others keys will still refer to the default values.

  1. Localization is supported. For more information, see Localization | Apple Developer Documentation.

Example CirclePWLocalizable.strings

/*
 * Loco ios export: iOS Localizable.strings
 * Project: strings.xml conversion
 * Release: Working copy
 * Locale: en, English
 * Exported by: Circle
 * Exported at: Tue, 27 Jun 2023 23:35:22 +0800
 */

// [General]
"circlepw_show_pin" = "Show PIN";
"circlepw_hide_pin" = "Hide PIN";
"circlepw_continue" = "Continue";
"circlepw_next" = "Next";
"circlepw_confirm" = "Confirm";

"circlepw_question" = "Question";
"circlepw_answer" = "Answer";
"circlepw_hint" = "Hint";
"circlepw_empty_placeholder" = "-";

// [Page] EnterPINCode
"circlepw_enter_pincode_headline" = "Enter your";
"circlepw_enter_pincode_headline_2" = "Web3 PIN";
"circlepw_enter_pincode_subhead" = "Let us know if it’s really you.";
"circlepw_enter_pincode_forgot_pin" = "Forgot PIN?";

// [Page] NewPINCode
"circlepw_new_pincode_headline" = "Enter your new";
"circlepw_new_pincode_headline_2" = "Web3 PIN";
"circlepw_new_pincode_subhead" = "Your PIN can’t have repeating (e.g. 000000) or consecutive (e.g. 123456) numbers.";

// [Page] ConfirmPINCode
"circlepw_confirm_pincode_headline" = "Re-enter your PIN to confirm";
"circlepw_confirm_pincode_headline_2" = "";
"circlepw_confirm_pincode_subhead" = "";

// [Page] SecurityIntros
"circlepw_security_intros_headline" = "Set up your";
"circlepw_security_intros_headline_2" = "Recovery Method";
"circlepw_security_intros_description" = "This is the only way to recover wallet access if you forget your PIN. Pick 2 security questions and provide answers for access recovery.";
"circlepw_security_intros_link" = "Learn more";

// [Page] SecurityQuestions
"circlepw_security_questions_title" = "Recovery method";
"circlepw_security_questions_question_header" = "Choose your %@ question";
"circlepw_security_questions_question_placeholder" = "Select one question";
"circlepw_security_questions_required_mark" = "*";
"circlepw_security_questions_answer_header" = "Provide an answer";
"circlepw_security_questions_answer_placeholder" = "Type your answer here";
"circlepw_security_questions_answer_hint_header" = "Provide an answer hint (optional)";
"circlepw_security_questions_answer_hint_placeholder" = "Type your hint here";

// [Page] SelectQuestion
"circlepw_select_question_title" = "Select one question";

// [Page] SecuritySummary
"circlepw_security_summary_title" = "Summary";

// [Page] SecurityConfirm
"circlepw_security_confirm_title" = "Confirmation";
"circlepw_security_confirm_headline" = "Keep your questions safe";
"circlepw_security_confirm_input_headline" = "Type “I agree” to proceed:";
"circlepw_security_confirm_input_placeholder" = "Type “I agree” here";
"circlepw_security_confirm_input_match" = "I agree";

// [Page] RecoverPINCode
"circlepw_recover_pincode_headline" = "Recover your";
"circlepw_recover_pincode_headline_2" = "Web3 PIN";
"circlepw_recover_pincode_subhead" = "Please enter the answer of the security questions provided below.";
"circlepw_recover_pincode_answer_input_header" = "Enter your answer here";
"circlepw_recover_pincode_answer_input_placeholder" = "Type your answer here";

CirclePWTheme.json

Setup

  • Copy the file CirclePWTheme.json into your main bundle. You can use Cmd + drag and drop.
  • Make sure you have selected the Target Membership.

Example: CirclePWTheme.json

{
  "font": {
    "ultraLight": "",
    "thin": "",
    "light": "",
    "regular": "CustomFont-Regular",
    "medium": "CustomFont-Medium",
    "semibold": "",
    "bold": "",
    "heavy": "",
    "black": ""
  },
  "color": {
    "background": "#FFFFFF",
    "divider": "#F0EFEF",
    "success": "#00B14F",
    "error": "#F55538",
    "error_background": "#FDF2F2",

    "text_main": "#1A1A1A",
    "text_main2": "#1C1C1C",
    "text_auxiliary": "#3D3D3D",
    "text_auxiliary2": "#707070",
    "text_summary": "#005339",
    "text_summary_highlight": "#005339",
    "text_placeholder": "#A3A3A3",
    "text_action": "#136FD8",
    "text_action_pressed": "#B3136FD8",

    "pin_dot_base": "#FFFFFF",
    "pin_dot_base_border": "#707070",
    "pin_dot_activated": "#3D3D3D",

    "input_border": "#E8E8E8",
    "input_border_focused": "#00B14F",
    "input_background_disabled": "#F5F5F5",

    "main_bt_text": "#FFFFFF",
    "main_bt_text_pressed": "#FFFFFF",
    "main_bt_text_disabled": "#BFBFBF",
    "main_bt_background": "#00B14F",
    "main_bt_background_pressed": "#005339",
    "main_bt_background_disabled": "#F5F5F5",

    "recover_pin_hint_title": "#005339",
    "recover_pin_hint_title_bg": "#EEF9F9",
    "recover_pin_hint": "#005339",

    "security_confirm_main_bg": "#3AB5EE",

    "title_gradients": [
      "#00B14F",
      "#23B64B",
      "#35BA47",
      "#43BE43",
      "#4FC23F",
      "#59C53C",
      "#62C838",
      "#6ACA34",
      "#71CD31",
      "#77CF2D",
      "#7DD02A",
      "#82D228",
      "#85D325",
      "#88D424",
      "#8AD522",
      "#8CD521"
    ]
  }
}

Programmatic Customization

To programmatically customize the UI refers to the following documents:

Example Code

extension WalletSdkAdapter: WalletSdkLayoutProvider {

    func securityQuestions() -> [SecurityQuestion] {
        return [
            SecurityQuestion(title: "What is your childhood nickname?", inputType: .text),
            SecurityQuestion(title: "What is the middle name of your oldest child?", inputType: .text),
            SecurityQuestion(title: "What is your favorite team?", inputType: .text),
            SecurityQuestion(title: "When was your birthday?", inputType: .datePicker),
            SecurityQuestion(title: "When is your marriage anniversary?", inputType: .datePicker),
        ]
    }

    func securityQuestionsRequiredCount() -> Int {
        return 2
    }

    func securityConfirmItems() -> [SecurityConfirmItem] {
        return [
            SecurityConfirmItem(image: UIImage(named: "img_info"),
                                text: "This is the only way to recover my account access."),
            SecurityConfirmItem(image: UIImage(named: "img_claim_success"),
                                text: "Circle won’t store my answers so it’s my responsibility to remember them."),
            SecurityConfirmItem(image: UIImage(named: "img_claim_success"),
                                text: "I will lose access to my wallet and my digital assets if I forget my answers."),
        ]
    }
    
    func displayDateFormat() -> String {
        return "yyyy/MM/dd"
    }
    
    func imageStore() -> ImageStore {
        let local: [ImageStore.Img: UIImage] = [
            .naviBack: UIImage(named: "ic_navi_back")!,
            .naviClose: UIImage(named: "ic_navi_close")!,
            .selectCheckMark: UIImage(named: "ic_checkmark")!,
            .dropdownArrow: UIImage(named: "ic_trailing_down")!,
            .errorInfo: UIImage(named: "ic_warning_alt")!,
            .securityIntroMain: UIImage(named: "img_security_intro")!,
            .securityConfirmMain: UIImage(named: "img_driver_blog")!
        ]

        let remote: [ImageStore.Img: URL] = [
            .securityIntroMain: URL(string: "https://www.circle.com/hs-fs/hubfs/Sundaes/810/global-payments-810x810.png")!,
            .securityConfirmMain: URL(string: "https://www.circle.com/hs-fs/hubfs/Sundaes/810/Trust-810x810.png")!,
        ]

        return ImageStore(local: local, remote: remote)
    }
    
    func themeFont() -> ThemeConfig.ThemeFont? {
        return ThemeConfig.ThemeFont(
            ultraLight: nil,
            thin: nil,
            light: "CustomFont-Light",
            regular: "CustomFont-Regular",
            medium: "CustomFont-Medium",
            semibold: "CustomFont-SemiBold",
            bold: "CustomFont-Bold",
            heavy: nil,
            black: nil
        )
    }
}

WalletSdkDelegate

By modifying the WalletSdkDelegate, you can get the current view controller and control the UI parameters in the controller at run time.

public protocol WalletSdkDelegate: AnyObject {

    /// Tells the delegate that the SDK is about to be presented in the controller.
    /// You can customize the layout as you wish dynamically.
    ///
    /// - Parameter controller: The UIViewController to be presented
    func walletSdk(willPresentController controller: UIViewController)

    ...
}

Example Code

extension WalletSdkAdapter: WalletSdkDelegate {

    func walletSdk(willPresentController controller: UIViewController) {
        print("willPresentController: \(controller)")

        if let controller = controller as? NewPINCodeViewController {
            controller.titleLabel1.text = "Hello World"
            controller.titleLabel1.textColor = .blue
            controller.titleLabel1.font = .systemFont(ofSize: 28, weight: .black)

        } 
        
        if let controller = controller as? SecurityConfirmViewController {
            controller.imageBgView.backgroundColor = .blue
            controller.imageView.contentMode = .scaleAspectFill
        }
    }
    
    ...
}

View Public Interface

Use Jump to Definition (Cmd + left click) to see the public interface of the view controller. Change the UI items as you need.

Jump to Definition
Public Interface

ErrorMessenger

 ApiError Interface

public struct ApiError: Error {

    public let errorCode: ErrorCode

    /// Error string from the SDK
    public let errorString: String

    /// Error string for UI display
    public var displayString: String
}

By modifying the ErrorMessenger, you can customize the ApiError messages. Your customization replaces the default ApiError.displayString for UI usage. You can also manage the localized error message.

public protocol ErrorMessenger {

    func getErrorString(_ code: ApiError.ErrorCode) -> String?
}
   
Example Code
import CircleProgrammableWalletSDK

class MyErrorMessenger: ErrorMessenger {

    func getErrorString(_ code: ApiError.ErrorCode) -> String? {
        switch code {
        case .hintsMatchAnswers:
            return "Your custom error message."

        case .networkError:
            return "Your custom error message."

        default:
            return nil
        }
    }
}

// ============ setErrorMessenger ============

WalletSdk.shared.setErrorMessenger(MyErrorMessenger())

ErrorCode and Messages Table

The following table shows the ApiError.ErrorCode key names and their default values:

ApiError.ErrorCodeDefault Value
unknown(-1)Unknown error
success(0)Success
apiParameterMissing(1)API parameter missing
apiParameterInvalid(2)API parameter invalid
forbidden(3)Forbidden
unauthorized(4)Unauthorized
retry(9)Retry
customerSuspended(10)Customer suspended
pending(11)Pending
invalidSession(12)Invalid session
invalidPartnerId(13)Invalid partner ID
invalidMessage(14)Invalid Message published to a SQS queue.
invalidPhone(15)Invalid phone number %@
walletIdNotFound(156001)
tokenIdNotFound(156002)
transactionIdNotFound(156003)
entityCredentialNotFound(156004)
walletSetIdNotFound(156005)
userAlreadyExisted(155101)user already existed
userNotFound(155102)user not found
userTokenNotFound(155103)user token not found
userTokenExpired(155104)user token expired
invalidUserToken(155105)invalid user token
userWasInitialized(155106)user was initialized
userHasSetPin(155107)user has set pin
userHasSetSecurityQuestion(155108)user has set security question
userWasDisabled(155109)user was disabled
userDoesNotSetPinYet(155110)user does not set pin yet
userDoesNotSetSecurityQuestionYet(155111)user does not set security questions yet
incorrectUserPin(155112)The PIN you entered is incorrect. You have %@ attempts left.
incorrectDeviceId(155113)incorrect device id
incorrectAppId(155114)app id not found
incorrectSecurityAnswers(155115)The answers you entered are incorrect. You have %@ attempts left.
invalidChallengeId(155116)invalid challenge id
invalidApproveContent(155117)invalid approve content
invalidEncryptionKey(155118)invalid encryption key
userPinLocked(155119)You’ve used up all PIN attempts. Please wait for %@ mins to retry later.
securityAnswersLocked(155120)The answers you entered are incorrect. Please wait for %@ mins to retry again.
walletIsFrozen(155501)Wallet is Frozen
maxWalletLimitReached(155502)Max wallet limit reached 
walletSetIdMutuallyExclusive(155503)WalletSetId can not be used together with blockchain and address filter
metadataUnmatched(155504)metadata array length does not match wallet count
userCanceled(155701)User canceled
launchUiFailed(155702)
pinCodeNotMatched(155703)The PIN you entered is not the same as the first one.
insecurePinCode(155704)Your PIN can’t have repeating or consecutive numbers.
hintsMatchAnswers(155705)Your hint can’t be the same as the answer.
networkError(155706)Network error