Skip to content

Commit 4f241ee

Browse files
authored
feat: iOS SDK subOrders 파라미터 추가 (#74)
* feat: 결제요청 subOrders 추가 * fix: Xcode 타깃에 SubOrder 추가 * feat: 샘플 위젯에 subOrders 입력 추가 * docs: subOrders 사용 예시 추가 * docs: 결제창 subOrders 예시 제거
1 parent 27015a1 commit 4f241ee

16 files changed

Lines changed: 292 additions & 12 deletions

Examples/TossPayments.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
6AB1E31D2A77976700D49332 /* EscrowProducts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB1E2DF2A77976700D49332 /* EscrowProducts.swift */; };
6464
6AB1E31E2A77976700D49332 /* CashReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB1E2E02A77976700D49332 /* CashReceipt.swift */; };
6565
6AB1E31F2A77976700D49332 /* CarrierCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB1E2E12A77976700D49332 /* CarrierCode.swift */; };
66+
49E80D8A6560494E8F9C5159 /* SubOrder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47A20FA8F96C4E908991C151 /* SubOrder.swift */; };
6667
6AB1E3202A77976700D49332 /* WidgetStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB1E2E22A77976700D49332 /* WidgetStatus.swift */; };
6768
6AB1E3212A77976700D49332 /* CustomRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB1E2E32A77976700D49332 /* CustomRequest.swift */; };
6869
6AB1E3222A77976700D49332 /* GiftCertificatePaymentInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AB1E2E52A77976700D49332 /* GiftCertificatePaymentInfo.swift */; };
@@ -206,6 +207,7 @@
206207
6AB1E2DF2A77976700D49332 /* EscrowProducts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EscrowProducts.swift; sourceTree = "<group>"; };
207208
6AB1E2E02A77976700D49332 /* CashReceipt.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CashReceipt.swift; sourceTree = "<group>"; };
208209
6AB1E2E12A77976700D49332 /* CarrierCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarrierCode.swift; sourceTree = "<group>"; };
210+
47A20FA8F96C4E908991C151 /* SubOrder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubOrder.swift; sourceTree = "<group>"; };
209211
6AB1E2E22A77976700D49332 /* WidgetStatus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WidgetStatus.swift; sourceTree = "<group>"; };
210212
6AB1E2E32A77976700D49332 /* CustomRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomRequest.swift; sourceTree = "<group>"; };
211213
6AB1E2E52A77976700D49332 /* GiftCertificatePaymentInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiftCertificatePaymentInfo.swift; sourceTree = "<group>"; };
@@ -511,6 +513,7 @@
511513
6AB1E2DF2A77976700D49332 /* EscrowProducts.swift */,
512514
6AB1E2E02A77976700D49332 /* CashReceipt.swift */,
513515
6AB1E2E12A77976700D49332 /* CarrierCode.swift */,
516+
47A20FA8F96C4E908991C151 /* SubOrder.swift */,
514517
);
515518
path = Etc;
516519
sourceTree = "<group>";
@@ -875,6 +878,7 @@
875878
6AB1E2F62A77976700D49332 /* BrowserPopupHandler.swift in Sources */,
876879
6AB1E3172A77976700D49332 /* AgreementStatus.swift in Sources */,
877880
6AB1E31F2A77976700D49332 /* CarrierCode.swift in Sources */,
881+
49E80D8A6560494E8F9C5159 /* SubOrder.swift in Sources */,
878882
6AB1E32A2A77976700D49332 /* WidgetService.swift in Sources */,
879883
6AB1E3072A77976700D49332 /* CustomRequestMessage.swift in Sources */,
880884
6AB1E3282A77976700D49332 /* VirtualAccountPaymentInfo.swift in Sources */,

Examples/UIKit-Example/Sources/Widget/PaymentWidgetViewController.swift

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ public final class PaymentWidgetViewController: ViewController {
2525
private lazy var amountInputField = TextField()
2626
private lazy var orderIdInputField = TextField()
2727
private lazy var orderNameInputField = TextField()
28+
private lazy var subOrderMerchantBusinessNumberInputField = TextField()
29+
private lazy var subOrderMerchantNameInputField = TextField()
30+
private lazy var subOrderMerchantCountryInputField = TextField()
31+
private lazy var subOrderMerchantPostalCodeInputField = TextField()
32+
private lazy var subOrderMerchantAddressInputField = TextField()
33+
private lazy var subOrderMerchantDetailAddressInputField = TextField()
34+
private lazy var subOrderNameInputField = TextField()
2835

2936
private lazy var widget: PaymentWidget = PaymentWidget(
3037
clientKey: Environment.clientKey,
@@ -72,6 +79,13 @@ public final class PaymentWidgetViewController: ViewController {
7279
stackView.addArrangedSubview(amountInputField)
7380
stackView.addArrangedSubview(orderIdInputField)
7481
stackView.addArrangedSubview(orderNameInputField)
82+
stackView.addArrangedSubview(subOrderMerchantBusinessNumberInputField)
83+
stackView.addArrangedSubview(subOrderMerchantNameInputField)
84+
stackView.addArrangedSubview(subOrderMerchantCountryInputField)
85+
stackView.addArrangedSubview(subOrderMerchantPostalCodeInputField)
86+
stackView.addArrangedSubview(subOrderMerchantAddressInputField)
87+
stackView.addArrangedSubview(subOrderMerchantDetailAddressInputField)
88+
stackView.addArrangedSubview(subOrderNameInputField)
7589
stackView.addArrangedSubview(paymentMethodWidget)
7690
stackView.addArrangedSubview(빈화면)
7791
stackView.addArrangedSubview(agreementWidget)
@@ -82,9 +96,25 @@ public final class PaymentWidgetViewController: ViewController {
8296
orderIdInputField.text = UUID().uuidString
8397
orderNameInputField.title = "orderName"
8498
orderNameInputField.text = "토스페이먼츠 세트"
99+
subOrderMerchantBusinessNumberInputField.title = "subOrders[0].merchantBusinessNumber"
100+
subOrderMerchantBusinessNumberInputField.text = "1234567890"
101+
subOrderMerchantNameInputField.title = "subOrders[0].merchantName"
102+
subOrderMerchantNameInputField.text = "토스 상점"
103+
subOrderMerchantCountryInputField.title = "subOrders[0].merchantAddress.country"
104+
subOrderMerchantCountryInputField.text = "KR"
105+
subOrderMerchantPostalCodeInputField.title = "subOrders[0].merchantAddress.postalCode"
106+
subOrderMerchantPostalCodeInputField.text = "06133"
107+
subOrderMerchantAddressInputField.title = "subOrders[0].merchantAddress.address"
108+
subOrderMerchantAddressInputField.text = "서울특별시 강남구 테헤란로"
109+
subOrderMerchantDetailAddressInputField.title = "subOrders[0].merchantAddress.detailAddress"
110+
subOrderMerchantDetailAddressInputField.text = "10층"
111+
subOrderNameInputField.title = "subOrders[0].orderName"
112+
subOrderNameInputField.text = "하위 주문"
85113

86114
amountInputField.textField.addTarget(self, action: #selector(textFieldDidChanged(_:)), for: .editingChanged)
87115
amountInputField.textField.keyboardType = .numberPad
116+
subOrderMerchantBusinessNumberInputField.textField.keyboardType = .numberPad
117+
subOrderMerchantPostalCodeInputField.textField.keyboardType = .numberPad
88118

89119
widget.delegate = self
90120
widget.paymentMethodWidget?.widgetUIDelegate = self
@@ -105,10 +135,39 @@ public final class PaymentWidgetViewController: ViewController {
105135
info: DefaultWidgetPaymentInfo(
106136
orderId: orderIdInputField.textField.text ?? UUID().uuidString,
107137
orderName: orderNameInputField.textField.text ?? "테스트 결제",
108-
appScheme: Environment.Constant.appScheme
138+
appScheme: Environment.Constant.appScheme,
139+
subOrders: [makeSubOrder()]
109140
)
110141
)
111142
}
143+
144+
private func makeSubOrder() -> SubOrder {
145+
SubOrder(
146+
merchantBusinessNumber: textValue(subOrderMerchantBusinessNumberInputField, default: "1234567890"),
147+
merchantName: textValue(subOrderMerchantNameInputField, default: "토스 상점"),
148+
merchantAddress: SubOrder.MerchantAddress(
149+
country: textValue(subOrderMerchantCountryInputField, default: "KR"),
150+
postalCode: textValue(subOrderMerchantPostalCodeInputField, default: "06133"),
151+
address: textValue(subOrderMerchantAddressInputField, default: "서울특별시 강남구 테헤란로"),
152+
detailAddress: optionalTextValue(subOrderMerchantDetailAddressInputField)
153+
),
154+
orderName: textValue(subOrderNameInputField, default: "하위 주문")
155+
)
156+
}
157+
158+
private func textValue(_ textField: TextField, default defaultValue: String) -> String {
159+
guard let text = textField.textField.text, !text.isEmpty else {
160+
return defaultValue
161+
}
162+
return text
163+
}
164+
165+
private func optionalTextValue(_ textField: TextField) -> String? {
166+
guard let text = textField.textField.text, !text.isEmpty else {
167+
return nil
168+
}
169+
return text
170+
}
112171
}
113172

114173
extension PaymentWidgetViewController {

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@ pod 'TossPayments'
3232
* [결제창 연동하기](https://docs.tosspayments.com/guides/payment/integration): 결제창에서 고객이 결제수단을 선택하고, 결제 정보를 입력해서 결제를 완료합니다.
3333
* [결제위젯 연동하기](https://docs.tosspayments.com/guides/payment-widget/integration): 토스페이먼츠에서 수많은 상점을 분석하여 만든 최적의 주문서 UI입니다.
3434

35+
### 하위 주문 정보 전달하기
36+
37+
하위 상점별 주문 정보가 필요하면 `subOrders``SubOrder` 배열을 전달하세요.
38+
39+
```swift
40+
let subOrder = SubOrder(
41+
merchantBusinessNumber: "1234567890",
42+
merchantName: "토스 상점",
43+
merchantAddress: SubOrder.MerchantAddress(
44+
country: "KR",
45+
postalCode: "06133",
46+
address: "서울특별시 강남구 테헤란로",
47+
detailAddress: "10층"
48+
),
49+
orderName: "하위 주문"
50+
)
51+
52+
let widgetPaymentInfo = DefaultWidgetPaymentInfo(
53+
orderId: UUID().uuidString,
54+
orderName: "토스페이먼츠 세트",
55+
subOrders: [subOrder]
56+
)
57+
```
58+
3559
### 샘플 프로젝트
3660

3761
1. `payment-sdk-ios` 리포지토리를 클론하세요.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Foundation
2+
3+
let subOrders = [
4+
SubOrder(
5+
merchantBusinessNumber: "1234567890",
6+
merchantName: "Toss Store",
7+
merchantAddress: SubOrder.MerchantAddress(
8+
country: "KR",
9+
postalCode: "06133",
10+
address: "서울특별시 강남구 테헤란로",
11+
detailAddress: "10층"
12+
),
13+
orderName: "하위 주문 A"
14+
),
15+
SubOrder(
16+
merchantBusinessNumber: "0987654321",
17+
merchantName: "Toss Branch",
18+
merchantAddress: SubOrder.MerchantAddress(
19+
country: "KR",
20+
postalCode: "06236",
21+
address: "서울특별시 강남구 강남대로",
22+
detailAddress: nil
23+
),
24+
orderName: "하위 주문 B"
25+
)
26+
]
27+
28+
let paymentInfo = DefaultPaymentInfo(
29+
amount: 1000,
30+
orderId: "order-id",
31+
orderName: "대표 주문",
32+
subOrders: subOrders
33+
)
34+
35+
guard let paymentSubOrders = paymentInfo.requestJSONObject?["subOrders"] as? [[String: Any]] else {
36+
fatalError("PaymentInfo request JSON must include subOrders")
37+
}
38+
39+
assert(paymentSubOrders.count == 2)
40+
assert(paymentSubOrders[0]["merchantBusinessNumber"] as? String == "1234567890")
41+
assert(paymentSubOrders[0]["merchantName"] as? String == "Toss Store")
42+
assert(paymentSubOrders[0]["orderName"] as? String == "하위 주문 A")
43+
44+
guard let firstAddress = paymentSubOrders[0]["merchantAddress"] as? [String: Any] else {
45+
fatalError("PaymentInfo subOrders must include merchantAddress")
46+
}
47+
48+
assert(firstAddress["country"] as? String == "KR")
49+
assert(firstAddress["postalCode"] as? String == "06133")
50+
assert(firstAddress["address"] as? String == "서울특별시 강남구 테헤란로")
51+
assert(firstAddress["detailAddress"] as? String == "10층")
52+
53+
guard let secondAddress = paymentSubOrders[1]["merchantAddress"] as? [String: Any] else {
54+
fatalError("PaymentInfo subOrders must include merchantAddress")
55+
}
56+
57+
assert(secondAddress["detailAddress"] == nil)
58+
59+
let orderedInfo = paymentInfo.orderedInfo
60+
61+
guard orderedInfo.contains("subOrders: [") else {
62+
fatalError("PaymentInfo orderedInfo must display subOrders as an object array")
63+
}
64+
65+
assert(orderedInfo.contains(" {\n merchantBusinessNumber: 1234567890"))
66+
assert(orderedInfo.contains(" merchantName: Toss Store"))
67+
assert(orderedInfo.contains(" merchantAddress: {"))
68+
assert(orderedInfo.contains(" country: KR"))
69+
assert(orderedInfo.contains(" postalCode: 06133"))
70+
assert(orderedInfo.contains(" address: 서울특별시 강남구 테헤란로"))
71+
assert(orderedInfo.contains(" detailAddress: 10층"))
72+
assert(orderedInfo.contains(" orderName: 하위 주문 A\n }"))
73+
assert(orderedInfo.contains("]"))
74+
75+
let widgetPaymentInfo = DefaultWidgetPaymentInfo(
76+
orderId: "widget-order-id",
77+
orderName: "위젯 대표 주문",
78+
subOrders: subOrders
79+
)
80+
81+
guard let widgetSubOrders = widgetPaymentInfo.convertToPaymentInfo(amount: 2000)?["subOrders"] as? [[String: Any]] else {
82+
fatalError("WidgetPaymentInfo request JSON must include subOrders")
83+
}
84+
85+
assert(widgetSubOrders.count == 2)
86+
assert(widgetSubOrders[1]["merchantBusinessNumber"] as? String == "0987654321")
87+
assert(widgetSubOrders[1]["merchantName"] as? String == "Toss Branch")
88+
assert(widgetSubOrders[1]["orderName"] as? String == "하위 주문 B")
89+
90+
print("SubOrders serialization check passed")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
//
2+
// SubOrder.swift
3+
//
4+
//
5+
// Created by TossPayments on 2026/05/14.
6+
//
7+
8+
import Foundation
9+
10+
public struct SubOrder: Codable {
11+
public struct MerchantAddress: Codable {
12+
public let country: String
13+
public let postalCode: String
14+
public let address: String
15+
public let detailAddress: String?
16+
17+
public init(
18+
country: String,
19+
postalCode: String,
20+
address: String,
21+
detailAddress: String? = nil
22+
) {
23+
self.country = country
24+
self.postalCode = postalCode
25+
self.address = address
26+
self.detailAddress = detailAddress
27+
}
28+
}
29+
30+
public let merchantBusinessNumber: String
31+
public let merchantName: String
32+
public let merchantAddress: MerchantAddress
33+
public let orderName: String
34+
35+
public init(
36+
merchantBusinessNumber: String,
37+
merchantName: String,
38+
merchantAddress: MerchantAddress,
39+
orderName: String
40+
) {
41+
self.merchantBusinessNumber = merchantBusinessNumber
42+
self.merchantName = merchantName
43+
self.merchantAddress = merchantAddress
44+
self.orderName = orderName
45+
}
46+
}

TossPayments/Sources/Model/PaymentInfo/CardPaymentInfo.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public struct CardPaymentInfo: PaymentInfo {
2828
public let customerName: String?
2929
public let customerEmail: String?
3030
public let taxFreeAmount: Double?
31+
public let subOrders: [SubOrder]?
3132

3233
public struct Plan: Codable {
3334
public let company: String
@@ -60,7 +61,8 @@ public struct CardPaymentInfo: PaymentInfo {
6061
currency: String? = nil,
6162
customerName: String? = nil,
6263
customerEmail: String? = nil,
63-
taxFreeAmount: Double? = nil
64+
taxFreeAmount: Double? = nil,
65+
subOrders: [SubOrder]? = nil
6466
) {
6567
self.amount = amount
6668
self.orderId = orderId
@@ -80,5 +82,6 @@ public struct CardPaymentInfo: PaymentInfo {
8082
self.customerName = customerName
8183
self.customerEmail = customerEmail
8284
self.taxFreeAmount = taxFreeAmount
85+
self.subOrders = subOrders
8386
}
8487
}

TossPayments/Sources/Model/PaymentInfo/DefaultPaymentInfo.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public struct DefaultPaymentInfo: PaymentInfo {
1919
public let customerEmail: String?
2020
public let taxFreeAmount: Double?
2121
public let cultureExpense: Bool
22+
public let subOrders: [SubOrder]?
2223

2324
public init(
2425
amount: Double,
@@ -27,7 +28,8 @@ public struct DefaultPaymentInfo: PaymentInfo {
2728
customerName: String? = nil,
2829
customerEmail: String? = nil,
2930
taxFreeAmount: Double? = nil,
30-
cultureExpense: Bool = false
31+
cultureExpense: Bool = false,
32+
subOrders: [SubOrder]? = nil
3133
) {
3234
self.amount = amount
3335
self.orderId = orderId
@@ -36,5 +38,6 @@ public struct DefaultPaymentInfo: PaymentInfo {
3638
self.customerEmail = customerEmail
3739
self.taxFreeAmount = taxFreeAmount
3840
self.cultureExpense = cultureExpense
41+
self.subOrders = subOrders
3942
}
4043
}

TossPayments/Sources/Model/PaymentInfo/ForeignEasyPayPaymentInfo.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public struct ForeignEasyPayPaymentInfo: PaymentInfo {
2020
public var customerName: String?
2121
public var customerEmail: String?
2222
public var taxFreeAmount: Double?
23+
public var subOrders: [SubOrder]?
2324

2425
public init(
2526
amount: Double,
@@ -31,7 +32,8 @@ public struct ForeignEasyPayPaymentInfo: PaymentInfo {
3132
pendingUrl: String,
3233
customerName: String? = nil,
3334
customerEmail: String? = nil,
34-
taxFreeAmount: Double? = nil
35+
taxFreeAmount: Double? = nil,
36+
subOrders: [SubOrder]? = nil
3537
) {
3638
self.amount = amount
3739
self.orderId = orderId
@@ -43,5 +45,6 @@ public struct ForeignEasyPayPaymentInfo: PaymentInfo {
4345
self.customerName = customerName
4446
self.customerEmail = customerEmail
4547
self.taxFreeAmount = taxFreeAmount
48+
self.subOrders = subOrders
4649
}
4750
}

TossPayments/Sources/Model/PaymentInfo/GiftCertificatePaymentInfo.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,23 @@ public struct GiftCertificatePaymentInfo: PaymentInfo {
1515
public let customerName: String?
1616
public let customerEmail: String?
1717
public let taxFreeAmount: Double?
18+
public let subOrders: [SubOrder]?
1819

1920
public init(
2021
amount: Double,
2122
orderId: String,
2223
orderName: String,
2324
customerName: String?,
2425
customerEmail: String?,
25-
taxFreeAmount: Double?
26+
taxFreeAmount: Double?,
27+
subOrders: [SubOrder]? = nil
2628
) {
2729
self.amount = amount
2830
self.orderId = orderId
2931
self.orderName = orderName
3032
self.customerName = customerName
3133
self.customerEmail = customerEmail
3234
self.taxFreeAmount = taxFreeAmount
35+
self.subOrders = subOrders
3336
}
3437
}

0 commit comments

Comments
 (0)