iOS SDK
Install the DEUNA iOS SDK and integrate modal, embedded, wallet, voucher, and next-action flows.
On this page
- Requirements
- Install
- Initialize
- Open Payment Widget
- Available experiences
- Embed a widget with SwiftUI
- Filter and configure payment methods
- Dynamic styling and order refresh
- Payment Vault and Click to Pay
- Native Apple Pay
- Continue a pending action
- Open a voucher
- Fraud-device data and fingerprinting
- Callback and lifecycle reference
- Official examples and source
Requirements#
Review the requirements for integrating DEUNA's iOS SDK:
- Swift version: 5.8 or higher
- Xcode version: 16.0 or higher
- Deployment target: iOS 13.0 or higher for core modal experiences; iOS 14.0 or higher for embedded SwiftUI
DeunaWidget - DEUNA credentials: Active account and valid credentials (contact support@deuna.com if you experience delays obtaining credentials)
Install#
There are two supported methods to install the DEUNA iOS SDK:
Swift Package Manager (Recommended)
- In Xcode, navigate to File → Add Package Dependencies...
- In the search bar, enter the repository URL:
Plain text
https://github.com/deuna-developers/deuna-sdk-ios - Pin the package dependency rule to version
2.12.6(or higher). - When the Choose Package Products dialog appears:
- Select
DeunaSDKand link it to your application target. - Set all other products (
DeunaSDKCybersource,DeunaSDKKount,DeunaSDKRiskified,DeunaSDKSift,DeunaSDKSignifyd,DeunaSDKMercadoPago,DeunaSDKAccertify) to None unless your integration uses that specific anti-fraud provider.
- Select
CocoaPods
If your iOS project uses CocoaPods:
- Specify the minimum deployment target and add
DeunaSDKto yourPodfile:
platform :ios, '14.0'
target 'YourApp' do
use_frameworks!
pod 'DeunaSDK', '~> 2.12.6'
# Add ONLY the optional fraud provider subspecs you need:
# pod 'DeunaSDK/MercadoPago'
# pod 'DeunaSDK/Riskified'
# pod 'DeunaSDK/Cybersource'
# pod 'DeunaSDK/Signifyd'
# pod 'DeunaSDK/Kount'
# pod 'DeunaSDK/Sift'
# pod 'DeunaSDK/Accertify'
end- Run:
pod install- Disable User Script Sandboxing: In Xcode, navigate to your target's Build Settings, search for
User Script Sandboxing(ENABLE_USER_SCRIPT_SANDBOXING), and set it to No.
Required Permissions for Voucher Downloads
Initialize#
import DeunaSDKYou can initialize the SDK in two ways depending on your app architecture:
Global shared instance
Initialize once during app startup (e.g., in your AppDelegate or App initializer), and access anywhere via DeunaSDK.shared:
DeunaSDK.initialize(
environment: .sandbox, // .sandbox or .production
publicApiKey: "YOUR_PUBLIC_API_KEY"
)
let deunaSDK = DeunaSDK.sharedCoordinator or screen instance
Create and retain an instance for a specific screen or coordinator:
let deunaSDK = DeunaSDK(
environment: .sandbox,
publicApiKey: "YOUR_PUBLIC_API_KEY"
)Open Payment Widget#
The Payment Widget offers multiple payment methods to customers:
- Credit and debit cards: Visa, Mastercard, American Express, and co-branded cards.
- Alternative payment methods (APMs): OXXO, KueskiPay, Aplazo, Daviplata, Nequi, and more.
- Digital wallets: Apple Pay and PayPal.
Display in a modal
Call initPaymentWidget on your DeunaSDK instance:
deunaSDK.initPaymentWidget(
orderToken: "YOUR_ORDER_TOKEN",
callbacks: PaymentWidgetCallbacks(
onSuccess: { order in
deunaSDK.close() // Close the payment widget modal
// YOUR CODE HERE: Handle successful payment (e.g. navigate to confirmation screen)
},
onError: { error in
// Error handling
if error.type == .initializationFailed {
deunaSDK.close() // Close the widget if initialization fails
// YOUR CODE HERE: Handle initialization failure (e.g. show alert or fallback)
return
}
if error.type == .paymentError {
// YOUR CODE HERE: Handle payment processing failure (e.g. prompt user to retry)
}
},
onClosed: { action in
// Widget closed
// action == .userAction (dismissed manually by customer) or .systemAction (closed via code)
// YOUR CODE HERE: Handle modal dismissal
},
onCardBinDetected: { metadata in
guard metadata != nil else { return }
// YOUR CODE HERE: Handle card brand or BIN detection (e.g. apply discounts)
},
onInstallmentSelected: { installment in
// YOUR CODE HERE: Handle installment plan selection
},
onPaymentProcessing: {
// YOUR CODE HERE: Payment processing started (e.g. disable external buttons)
},
onEventDispatch: { event, payload in
// YOUR CODE HERE: Track analytics event
}
),
userToken: "YOUR_USER_TOKEN", // optional
styleFile: "YOUR_STYLE_FILE_UUID", // optional
language: "en" // optional: "es", "en", "pt"
)Parameter Reference
| Parameter | Type | Modal | Embedded | Description |
|---|---|---|---|---|
orderToken | String | ✅ | ✅ | Required. Unique token for the order created via DEUNA API. Important: Do not define a redirect_url in order creation if you want onSuccess to run in-app. |
callbacks | PaymentWidgetCallbacks | ✅ | ✅ | Required. Callbacks listening to widget events (onSuccess, onError, onClosed, onCardBinDetected, onInstallmentSelected, onPaymentProcessing, onEventDispatch). |
userToken | String? | ✅ | ✅ | Optional. Customer's DEUNA bearer token. Important: For saved cards to appear, the email linked to userToken must match billing_address.email from order creation. |
styleFile | String? | ✅ | ✅ | Optional. UUID provided by DEUNA to apply custom theme branding (colors, typography, logo). |
paymentMethods | [[String: Any]]? | ✅ | ✅ | Optional. Array defining a single payment method for direct forms flow (e.g. [["paymentMethod": "bnpl", "processors": ["kueski"]]]). |
language | String? | ✅ | ✅ | Optional. Language code ("es", "en", "pt"). Overrides merchant default configuration. |
behavior | WidgetBehavior? | ✅ | ✅ | Optional. Flow customization, such as WidgetBehavior(paymentMethods: ["flowType": "twoStep"]) for PayPal, or Split Payments WidgetBehavior(paymentMethods: ["creditCard": ["splitPayments": ["maxCards": 2]]]). |
fraudCredentials | [String: Any]? | ✅ | ✅ | Optional. Fraud provider credentials (e.g. ["RISKIFIED": ["storeDomain": "yourdomain.com"]]). |
domain | String? | ✅ | ✅ | Optional. Overrides default base host (https://pay.deuna.io). |
hidePayButton | Bool | ❌ | ✅ | Optional. Hides the built-in pay button in embedded mode when your application renders a native button. |
Available experiences#
| Experience | Method | Configuration type | Important input |
|---|---|---|---|
| Payment Widget | initPaymentWidget(...) | PaymentWidgetConfiguration | orderToken, PaymentWidgetCallbacks |
| Payment Vault | initElements(...) | ElementsWidgetConfiguration | ElementsCallbacks; types may be omitted |
| Click to Pay | initElements(...) | ElementsWidgetConfiguration | types: [["name": ElementsWidget.clickToPay]] |
| Next Action | initNextAction(...) | NextActionWidgetConfiguration | Token for the order requiring another action |
| Voucher | initVoucher(...) | VoucherWidgetConfiguration | Token for the voucher order |
Embed a widget with SwiftUI#
On iOS 14 or later, use DeunaWidget to render the payment experience embedded in your SwiftUI hierarchy.
import DeunaSDK
import SwiftUI
struct EmbeddedCheckoutScreen: View {
let deunaSDK: DeunaSDK
let orderToken: String
var body: some View {
VStack {
DeunaWidget(
deunaSDK: deunaSDK,
configuration: PaymentWidgetConfiguration(
orderToken: orderToken,
callbacks: PaymentWidgetCallbacks(
onSuccess: { order in
// Explicitly release widget resources to prevent memory leaks
deunaSDK.dispose()
// YOUR CODE HERE: Handle successful payment
},
onError: { error in
if error.type == .paymentError {
// YOUR CODE HERE: Handle payment error
}
},
onEventDispatch: { event, payload in
// YOUR CODE HERE: Track analytics event
}
),
userToken: nil,
styleFile: nil
)
)
}
}
}Embedding with auto-resize and keyboard handling
By default, DeunaWidget fills its container at a fixed height. When passing autoResizeConfig: AutoResizeConfig(initialHeight: 100), the widget switches to auto-resize mode: it grows and shrinks dynamically to match the WebView content height.
When embedded inside a ScrollView, use ScrollViewFinder to capture the underlying UIScrollView and wire onScrollNeeded so focused fields automatically scroll above the keyboard:
import DeunaSDK
import SwiftUI
import UIKit
struct MyCheckoutView: View {
let deunaSDK: DeunaSDK
let widgetConfig: DeunaWidgetConfiguration
@State private var outerScrollView: UIScrollView?
var body: some View {
ScrollView {
VStack(spacing: 0) {
// Invisible helper capturing the underlying UIScrollView
Color.clear.frame(height: 0)
.background(ScrollViewFinder { sv in outerScrollView = sv })
// Your content above the widget
DeunaWidget(deunaSDK: deunaSDK, configuration: widgetConfig)
.onScrollNeeded { overlap in
guard let sv = outerScrollView else { return }
let newOffset = CGPoint(x: 0, y: sv.contentOffset.y + overlap)
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut) {
sv.setContentOffset(newOffset, animated: false)
}
}
// Your content below the widget
}
}
}
}
/// Traverses UIKit view hierarchy to capture the parent UIScrollView
struct ScrollViewFinder: UIViewRepresentable {
let onFound: (UIScrollView) -> Void
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.isHidden = true
DispatchQueue.main.async {
var parent = view.superview
while let p = parent {
if let sv = p as? UIScrollView { onFound(sv); return }
parent = p.superview
}
}
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}Custom pay button (hidePayButton)
If your application renders its own native button outside the widget, pass hidePayButton: true and execute payments manually using isValid and submit:
let configuration = PaymentWidgetConfiguration(
orderToken: orderToken,
callbacks: callbacks,
hidePayButton: true
)
// When the user taps your native pay button:
deunaSDK.isValid { isValid in
guard isValid else {
showValidationWarning()
return
}
deunaSDK.submit { result in
if result["status"] as? String == "error" {
print("Submit failed:", result["message"] ?? "")
}
}
}| Method | Description | Return Value |
|---|---|---|
deunaSDK.isValid { valid in ... } | Validates user input in active payment form. | Bool |
deunaSDK.submit { result in ... } | Submits payment equivalent to clicking the widget button. | [String: Any] (status: "success" or status: "error") |
Filter and configure payment methods#
Direct forms flow (paymentMethods)
The DEUNA checkout architectures support launching directly into an individual payment form—a pattern known as the forms flow.
While paymentMethods is an array type, the checkout engines currently support only one element in this list. Supplying a single payment method opens the widget directly into that method's form (e.g. Kueski Pay BNPL), bypassing the method selector.
deunaSDK.initPaymentWidget(
orderToken: orderToken,
callbacks: callbacks,
paymentMethods: [
[
"paymentMethod": "bnpl",
"processors": ["kueski"]
]
]
)Configuration priority resolution
| Priority | Source | Behavior |
|---|---|---|
| 1 (Highest) | paymentMethods in .initPaymentWidget() | Direct forms flow: Opens immediately into the single specified payment method form (e.g., Kueski), bypassing the method selector. |
| 2 | include_payment_options in order creation | Multi-method selection: Displays only the payment methods specified during order creation that are active on your account. |
| 3 (Fallback) | Merchant-level configuration (/payment-methods) | Full selector: If omitted in both SDK call and order payload, displays all active payment methods configured for your merchant account. |
PayPal configurations
Flow Type (flowType)
deunaSDK.initPaymentWidget(
orderToken: orderToken,
behavior: WidgetBehavior(
paymentMethods: [
"flowType": "twoStep" // "twoStep" (summary first) or "singleStep" (direct)
]
)
)Auto Purchase (express)
When customers have previously authorized quick payments with PayPal, enable immediate processing:
deunaSDK.initPaymentWidget(
orderToken: orderToken,
callbacks: callbacks,
paymentMethods: [
[
"paymentMethod": "wallet",
"processors": ["paypal_wallet"],
"configuration": ["express": true]
]
]
)Dynamic styling and order refresh#
Dynamically customize appearance (setCustomStyle)
Update widget styles dynamically (e.g. adapting branding upon card BIN detection):
deunaSDK.setCustomStyle(data: [
"theme": [
"colors": [
"primaryTextColor": "#023047",
"buttonPrimaryFill": "#FFB703",
"buttonPrimaryText": "#000000"
]
]
])Refresh order totals (refetchOrder)
When offering franchise-specific promotions (e.g., "20% off with Mastercard"), listen to onCardBinDetected, update the order on your backend, and notify the widget:
callbacks: PaymentWidgetCallbacks(
onCardBinDetected: { metadata in
guard let brand = metadata?["cardBrand"] as? String, brand == "Mastercard" else { return }
Task {
await applyDiscountOnBackend(orderId, promoCode: "MASTERCARD_20")
deunaSDK.refetchOrder { updatedOrder in
print("Order refreshed with discount:", updatedOrder)
}
}
}
)Payment Vault and Click to Pay#
Payment Vault is a component of DEUNA Payment solutions that allows saving cards securely and paying via Click to Pay without processing regular checkouts.
Display in a modal
deunaSDK.initElements(
userToken: "<USER_TOKEN>", // optional if userInfo is provided
userInfo: DeunaSDK.UserInfo(
firstName: "Esteban", // optional if userToken is provided
lastName: "Posada", // optional if userToken is provided
email: "esteban@deuna.com" // required
),
callbacks: ElementsCallbacks(
onSuccess: { response in
deunaSDK.close() // Close the Vault widget
// YOUR CODE HERE: Handle card saved successfully
},
onError: { error in
deunaSDK.close() // Close the Vault widget on error
// YOUR CODE HERE: Handle error
},
onClosed: { action in
// Widget closed: action == .userAction or .systemAction
// YOUR CODE HERE: Handle widget dismissal
},
onEventDispatch: { type, response in
// YOUR CODE HERE: Listen to widget events
}
),
orderToken: "YOUR_ORDER_TOKEN", // optional
widgetExperience: ElementsWidgetExperience( // optional
userExperience: ElementsWidgetExperience.UserExperience(
showSavedCardFlow: true, // optional: show card saving toggle
defaultCardFlow: true // optional: show toggle to save as default
)
)
)Display embedded (SwiftUI)
DeunaWidget(
deunaSDK: deunaSDK,
configuration: ElementsWidgetConfiguration(
callbacks: ElementsCallbacks(
onSuccess: { response in
deunaSDK.dispose() // Explicitly release widget resources
// YOUR CODE HERE: Handle success
},
onError: { error in
// YOUR CODE HERE: Handle error
},
onEventDispatch: { type, response in
// YOUR CODE HERE: Listen to widget events
}
),
userInfo: DeunaSDK.UserInfo(
firstName: "Esteban",
lastName: "Posada",
email: "esteban@deuna.com"
),
orderToken: "YOUR_ORDER_TOKEN",
autoResizeConfig: AutoResizeConfig(initialHeight: 150)
)
)Click to Pay
deunaSDK.initElements(
userInfo: DeunaSDK.UserInfo( // mandatory for Click To Pay
firstName: "Esteban",
lastName: "Posada",
email: "esteban@deuna.com"
),
callbacks: ElementsCallbacks(
onSuccess: { data in
deunaSDK.close() // Close the widget
// YOUR CODE HERE: Handle Click to Pay success
},
onError: { error in
deunaSDK.close() // Close the widget
// YOUR CODE HERE: Handle error
},
onClosed: { action in
// YOUR CODE HERE: Handle widget dismissal
},
onEventDispatch: { type, data in
// YOUR CODE HERE: Listen to widget events
}
),
types: [
[
"name": ElementsWidget.clickToPay // Pass this to render Click to Pay
]
]
)Parameter Reference
| Parameter | Type | Modal | Embedded | Description |
|---|---|---|---|---|
callbacks | ElementsCallbacks | ✅ | ✅ | Required. Event listeners (onSuccess, onError, onClosed, onEventDispatch). |
userInfo | DeunaSDK.UserInfo? | ✅ | ✅ | Customer's firstName, lastName, and email. Required for Click to Pay. |
userToken | String? | ✅ | ✅ | Optional. DEUNA bearer token. If passed, omit userInfo. |
types | [[String: Any]]? | ✅ | ✅ | Optional. Omit to render the default Card Vault experience. Pass [["name": ElementsWidget.clickToPay]] to render Click to Pay. |
orderToken | String? | ✅ | ✅ | Optional. Unique order token. Customer details can be extracted from billing_address. |
styleFile | String? | ✅ | ✅ | Optional. Theme customization UUID. |
widgetExperience | ElementsWidgetExperience? | ✅ | ✅ | Optional. showSavedCardFlow (card saving toggle) and defaultCardFlow (save as default toggle). |
domain | String? | ✅ | ✅ | Optional. Overrides host (https://elements.deuna.com). |
hidePayButton | Bool | ❌ | ✅ | Optional. Hides button when embedded. Use isValid and submit for custom buttons. |
Native Apple Pay#
Check merchant configuration and device capability before rendering an Apple Pay button.
let userInfo = DeunaSDK.UserInfo(email: "ada@example.com")
let params = GetWalletsAvailableParams(
orderToken: "YOUR_ORDER_TOKEN",
userInfo: userInfo
)
deunaSDK.getWalletsAvailable(params: params) { wallets, error in
if let error = error {
// YOUR CODE HERE: Handle wallet availability check error
return
}
// Check if Apple Pay is available on this device and merchant account
let isApplePayAvailable = wallets.contains(.applePay)
// YOUR CODE HERE: Update Apple Pay button visibility in UI
}After availability succeeds, launch the native Apple Pay sheet using initElements:
deunaSDK.initElements(
userInfo: userInfo,
callbacks: ElementsCallbacks(
onSuccess: { data in
// YOUR CODE HERE: Handle Apple Pay authorization success
},
onError: { error in
// YOUR CODE HERE: Handle Apple Pay error
},
onClosed: { action in
// YOUR CODE HERE: Handle Apple Pay sheet dismissal
}
),
types: [
["name": "APPLE_PAY"]
],
orderToken: "YOUR_ORDER_TOKEN"
)The application must include the Apple Pay entitlement and a provisioning profile containing the merchant identifier. See Apple Pay for the full setup.
Continue a pending action#
Use the same order token when DEUNA reports that the payment requires a supported 3DS challenge or redirect.
deunaSDK.initNextAction(
orderToken: "YOUR_ORDER_TOKEN",
callbacks: NextActionCallbacks(
onSuccess: { order in
deunaSDK.close()
// YOUR CODE HERE: Handle 3DS or action completion
},
onError: { error in
deunaSDK.close()
// YOUR CODE HERE: Handle error
},
onClosed: { action in
// YOUR CODE HERE: Handle modal dismissal
}
)
)The SDK presents supported external URLs and returns control to the widget. Use callback state for the interface and the authoritative DEUNA payment state for fulfillment.
Open a voucher#
deunaSDK.initVoucher(
orderToken: "YOUR_ORDER_TOKEN",
callbacks: VoucherCallbacks(
onSuccess: { order in
// YOUR CODE HERE: Handle voucher display
},
onError: { error in
// YOUR CODE HERE: Handle error
},
onClosed: { action in
// YOUR CODE HERE: Handle modal dismissal
}
)
)Voucher callbacks follow the same success, error, close, and event-dispatch lifecycle as the other payment experiences.
Fraud-device data and fingerprinting#
The DEUNA iOS SDK uses a modular architecture: it does not bundle third-party fraud SDKs by default. Only add the specific provider(s) you plan to use. If you don't use a provider, you don't pay any app size penalty.
Supported Providers
| Provider | SPM Product | CocoaPods Subspec | Required Parameters |
|---|---|---|---|
MERCADOPAGO | DeunaSDKMercadoPago | pod 'DeunaSDK/MercadoPago' | (None required - pass [:]) |
RISKIFIED | DeunaSDKRiskified | pod 'DeunaSDK/Riskified' | storeDomain |
CYBERSOURCE | DeunaSDKCybersource | pod 'DeunaSDK/Cybersource' | orgId, merchantId (Optional: fpServer) |
SIGNIFYD | DeunaSDKSignifyd | pod 'DeunaSDK/Signifyd' | orgId (Optional: fpServer) |
KOUNT | DeunaSDKKount | pod 'DeunaSDK/Kount' | merchantId, clientId, dataCollectorUrl, environment |
SIFT | DeunaSDKSift | pod 'DeunaSDK/Sift' | accountId, beaconKey |
ACCERTIFY | DeunaSDKAccertify | pod 'DeunaSDK/Accertify' | Config files in Xcode bundle resources |
Accertify bundle assets setup
The Accertify integration requires two configuration files inside your Xcode project. Ensure both files are added to your main target and included under Copy Bundle Resources:
server_keys_message_hosted.json: The public encryption keys file provided by Accertify.InMobile.plist: Property list file containing your merchant details:accountID(String): Your account GUIDregistrationURL(String): InMobile registration endpointlogURL(String): InMobile log endpointserverKeyData(String):server_keys_message_hosted.json
Direct call via generateFraudId
Call deunaSDK.generateFraudId with the parameters for your contracted providers:
deunaSDK.generateFraudId(params: [
"RISKIFIED": [
"storeDomain": "yourdomain.com"
],
"CYBERSOURCE": [
"orgId": "your_org_id",
"merchantId": "your_merchant_id"
],
"MERCADOPAGO": [:],
"ACCERTIFY": [:]
]) { fraudId in
// fraudId is a Base64-encoded JSON string, or nil if providers failed or none configured
guard let fraudId = fraudId else { return }
print("Base64 Fraud ID:", fraudId)
}Pass fraudCredentials directly to widgets
When launching widgets (initPaymentWidget, PaymentWidgetConfiguration, etc.), you can pass your configuration directly in fraudCredentials. The SDK generates the fraud ID internally at purchase time:
deunaSDK.initPaymentWidget(
orderToken: orderToken,
callbacks: callbacks,
fraudCredentials: [
"RISKIFIED": [
"storeDomain": "yourdomain.com"
],
"CYBERSOURCE": [
"orgId": "your_org_id",
"merchantId": "your_merchant_id"
],
"MERCADOPAGO": [:]
]
)Behavior notes
- Zero-bloat & Crash-safe: If a provider is included in the dictionary request but its framework or subspec was not linked, the SDK safely ignores it without throwing an exception or crashing.
- Parallel execution: Requested providers profile concurrently on background threads.
- Fallback behavior: If no valid providers are configured or available on the device, the completion handler receives
nil.
Callback and lifecycle reference#
| Callback / Method | Modal | Embedded | Description |
|---|---|---|---|
onSuccess | ✅ | ✅ | Payment or card vault operation completed successfully. Receives order or credential payload. Call deunaSDK.close() (modal) or deunaSDK.dispose() (embedded). |
onError | ✅ | ✅ | Error occurred during initialization or payment processing. Receives typed PaymentsError or ElementsError. |
onClosed | ✅ | ❌ | Modal closed. Receives CloseAction.userAction (dismissed manually) or CloseAction.systemAction (closed via close()). |
onPaymentProcessing | ✅ | ✅ | Triggered when customer taps pay and payment processing begins. Use to disable buttons and show progress spinners. |
onCardBinDetected | ✅ | ✅ | Triggered when a card BIN is detected or cleared (nil when user deletes the number). Use for franchise discounts. |
onInstallmentSelected | ✅ | ✅ | Triggered when customer selects deferred installments (nil on full one-time payment). |
onEventDispatch | ✅ | ✅ | Emits all underlying CheckoutEvent or ElementsEvent occurrences with data payloads. |
close() | ✅ | ❌ | Dismisses the active modal presentation while retaining the SDK instance. |
dispose() | ❌ | ✅ | Releases the embedded controller and WebView when the view hierarchy unmounts. |
Handling errors (onError)
Inspect error.type to distinguish initialization failures from processing declines:
onError: { error in
switch error.type {
case .initializationFailed:
// Widget failed to load; close presentation and notify user
deunaSDK.close()
showToast("Payment service temporarily unavailable")
case .paymentError:
// Transaction declined or processor error
showRetry(error)
@unknown default:
showGenericError(error)
}
}Official examples and source#
See WalletsScreen.swift for native Apple Pay and EmbeddedScreen.swift for the SwiftUI lifecycle.