Skip to main content

Custom STC Pay UI

Build your own STC Pay form while using the SDK's STCPayViewModel for payment processing.


Overview

The STCPayViewModel exposes all the logic you need for STC Pay payments. Your custom view only needs to:

  1. Display the appropriate step (mobile number or OTP)
  2. Call the view model's methods
  3. Listen to published state changes

Step 1: Create a Payment Request

import MoyasarSdk

func createSTCPaymentRequest() -> PaymentRequest {
do {
return try PaymentRequest(
apiKey: "pk_test_YOUR_API_KEY",
amount: 1000,
currency: "SAR",
description: "Order #12345",
metadata: [
"order_id": .stringValue("ios_order_3214124")
]
)
} catch {
fatalError("Invalid API key: \(error)")
}
}

Step 2: Create Your Custom View

Initialize your view with a PaymentRequest and a result callback:

import SwiftUI
import MoyasarSdk

struct MyCustomSTCPayView: View {
@ObservedObject var viewModel: STCPayViewModel

init(paymentRequest: PaymentRequest, callback: @escaping STCResultCallback) {
self._viewModel = ObservedObject(
wrappedValue: STCPayViewModel(
paymentRequest: paymentRequest,
resultCallback: callback
)
)
}

var body: some View {
VStack(spacing: 16) {
switch viewModel.screenStep {
case .mobileNumber:
phoneStep
case .otp:
otpStep
}
}
.padding()
}
}

Step 3: Build the Mobile Number Step

private var phoneStep: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Mobile Number")
.font(.headline)

TextField("05XXXXXXXX", text: $viewModel.mobileNumber)
.keyboardType(.phonePad)
.textFieldStyle(.roundedBorder)

if !viewModel.isValidPhoneNumber && viewModel.showErrorHintView.value {
Text("Invalid phone number")
.foregroundColor(.red)
.font(.caption)
}

Button(action: {
Task {
await viewModel.initiatePayment()
}
}) {
if viewModel.isLoading {
ProgressView()
} else {
Text("Pay")
.frame(maxWidth: .infinity)
}
}
.disabled(!viewModel.isValidPhoneNumber)
.buttonStyle(.borderedProminent)
}
}

Step 4: Build the OTP Step

private var otpStep: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Enter OTP")
.font(.headline)

TextField("XXXXXX", text: $viewModel.otp)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)

if !viewModel.isValidOtp && viewModel.showErrorHintView.value {
Text("Invalid OTP")
.foregroundColor(.red)
.font(.caption)
}

Button(action: {
Task {
await viewModel.submitOtp()
}
}) {
if viewModel.isLoading {
ProgressView()
} else {
Text("Confirm")
.frame(maxWidth: .infinity)
}
}
.disabled(!viewModel.isValidOtp)
.buttonStyle(.borderedProminent)
}
}

STCPayViewModel Properties

Published Properties

PropertyTypeDescription
mobileNumberStringUser's phone number (bind to your text field)
otpStringUser's OTP code (bind to your text field)
screenStepSTCStepCurrent step: .mobileNumber or .otp
isLoadingBoolTrue while a network request is in progress
isValidPhoneNumberBoolTrue if phone number passes validation
isValidOtpBoolTrue if OTP passes validation
showErrorHintViewBoolWhether to show error hints

Methods

MethodDescription
initiatePayment()Submit the mobile number to initiate STC Pay payment
submitOtp()Submit the OTP to complete the payment

Step 5: Handle the Payment Result

func handleSTCResult(_ result: Result<ApiPayment, MoyasarError>) {
switch result {
case .success(let payment):
switch payment.status {
case .paid:
print("STC Pay successful: \(payment.id)")
case .failed:
print("STC Pay failed")
default:
print("STC Pay status: \(payment.status)")
}
case .failure(let error):
print("STC Pay error: \(error)")
}
}

Complete Examples

See the full implementations in the demo projects:


Next Steps