Skip to content
This repository was archived by the owner on Oct 16, 2025. It is now read-only.

Commit e83cea5

Browse files
authored
feat: enhanced external purchase apis (#15)
- Add iOS External Purchase Notice Sheet (iOS 18.2+) - canPresentExternalPurchaseNoticeIOS query - presentExternalPurchaseNoticeSheetIOS mutation - ExternalPurchaseNoticeResultIOS and ExternalPurchaseNoticeAction types - Add Android User Choice Billing event - userChoiceBillingAndroid subscription listener - UserChoiceBillingDetails type with external transaction token - Remove deprecated externalPurchaseUrl parameter from iOS purchase/subscription props
1 parent 11737ab commit e83cea5

File tree

8 files changed

+468
-46
lines changed

8 files changed

+468
-46
lines changed

src/api-ios.graphql

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ extend type Query {
1212
# Future
1313
getPromotedProductIOS: ProductIOS
1414
"""
15+
Check if external purchase notice sheet can be presented (iOS 18.2+)
16+
"""
17+
# Future
18+
canPresentExternalPurchaseNoticeIOS: Boolean!
19+
"""
1520
Retrieve all pending transactions in the StoreKit queue
1621
"""
1722
# Future
@@ -94,4 +99,14 @@ extend type Mutation {
9499
"""
95100
# Future
96101
presentCodeRedemptionSheetIOS: Boolean!
102+
"""
103+
Present external purchase notice sheet (iOS 18.2+)
104+
"""
105+
# Future
106+
presentExternalPurchaseNoticeSheetIOS: ExternalPurchaseNoticeResultIOS!
107+
"""
108+
Present external purchase custom link with StoreKit UI (iOS 18.2+)
109+
"""
110+
# Future
111+
presentExternalPurchaseLinkIOS(url: String!): ExternalPurchaseLinkResultIOS!
97112
}

src/event.graphql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ extend type Subscription {
1313
Fires when the App Store surfaces a promoted product (iOS only)
1414
"""
1515
promotedProductIOS: String!
16+
"""
17+
Fires when a user selects alternative billing in the User Choice Billing dialog (Android only)
18+
Only triggered when the user selects alternative billing instead of Google Play billing
19+
"""
20+
userChoiceBillingAndroid: UserChoiceBillingDetails!
1621
}

src/generated/Types.kt

Lines changed: 145 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,34 @@ public enum class ErrorCode(val rawValue: String) {
189189
fun toJson(): String = rawValue
190190
}
191191

192+
/**
193+
* User actions on external purchase notice sheet (iOS 18.2+)
194+
*/
195+
public enum class ExternalPurchaseNoticeAction(val rawValue: String) {
196+
/**
197+
* User chose to continue to external purchase
198+
*/
199+
Continue("continue"),
200+
/**
201+
* User dismissed the notice sheet
202+
*/
203+
Dismissed("dismissed")
204+
205+
companion object {
206+
fun fromJson(value: String): ExternalPurchaseNoticeAction = when (value) {
207+
"continue" -> ExternalPurchaseNoticeAction.Continue
208+
"CONTINUE" -> ExternalPurchaseNoticeAction.Continue
209+
"Continue" -> ExternalPurchaseNoticeAction.Continue
210+
"dismissed" -> ExternalPurchaseNoticeAction.Dismissed
211+
"DISMISSED" -> ExternalPurchaseNoticeAction.Dismissed
212+
"Dismissed" -> ExternalPurchaseNoticeAction.Dismissed
213+
else -> throw IllegalArgumentException("Unknown ExternalPurchaseNoticeAction value: $value")
214+
}
215+
}
216+
217+
fun toJson(): String = rawValue
218+
}
219+
192220
public enum class IapEvent(val rawValue: String) {
193221
PurchaseUpdated("purchase-updated"),
194222
PurchaseError("purchase-error"),
@@ -675,6 +703,66 @@ public data class EntitlementIOS(
675703
)
676704
}
677705

706+
/**
707+
* Result of presenting an external purchase link (iOS 18.2+)
708+
*/
709+
public data class ExternalPurchaseLinkResultIOS(
710+
/**
711+
* Optional error message if the presentation failed
712+
*/
713+
val error: String? = null,
714+
/**
715+
* Whether the user completed the external purchase flow
716+
*/
717+
val success: Boolean
718+
) {
719+
720+
companion object {
721+
fun fromJson(json: Map<String, Any?>): ExternalPurchaseLinkResultIOS {
722+
return ExternalPurchaseLinkResultIOS(
723+
error = json["error"] as String?,
724+
success = json["success"] as Boolean,
725+
)
726+
}
727+
}
728+
729+
fun toJson(): Map<String, Any?> = mapOf(
730+
"__typename" to "ExternalPurchaseLinkResultIOS",
731+
"error" to error,
732+
"success" to success,
733+
)
734+
}
735+
736+
/**
737+
* Result of presenting external purchase notice sheet (iOS 18.2+)
738+
*/
739+
public data class ExternalPurchaseNoticeResultIOS(
740+
/**
741+
* Optional error message if the presentation failed
742+
*/
743+
val error: String? = null,
744+
/**
745+
* Notice result indicating user action
746+
*/
747+
val result: ExternalPurchaseNoticeAction
748+
) {
749+
750+
companion object {
751+
fun fromJson(json: Map<String, Any?>): ExternalPurchaseNoticeResultIOS {
752+
return ExternalPurchaseNoticeResultIOS(
753+
error = json["error"] as String?,
754+
result = ExternalPurchaseNoticeAction.fromJson(json["result"] as String),
755+
)
756+
}
757+
}
758+
759+
fun toJson(): Map<String, Any?> = mapOf(
760+
"__typename" to "ExternalPurchaseNoticeResultIOS",
761+
"error" to error,
762+
"result" to result.toJson(),
763+
)
764+
}
765+
678766
public sealed interface FetchProductsResult
679767

680768
public data class FetchProductsResultProducts(val value: List<Product>?) : FetchProductsResult
@@ -1533,6 +1621,37 @@ public data class SubscriptionStatusIOS(
15331621
)
15341622
}
15351623

1624+
/**
1625+
* User Choice Billing event details (Android)
1626+
* Fired when a user selects alternative billing in the User Choice Billing dialog
1627+
*/
1628+
public data class UserChoiceBillingDetails(
1629+
/**
1630+
* Token that must be reported to Google Play within 24 hours
1631+
*/
1632+
val externalTransactionToken: String,
1633+
/**
1634+
* List of product IDs selected by the user
1635+
*/
1636+
val products: List<String>
1637+
) {
1638+
1639+
companion object {
1640+
fun fromJson(json: Map<String, Any?>): UserChoiceBillingDetails {
1641+
return UserChoiceBillingDetails(
1642+
externalTransactionToken = json["externalTransactionToken"] as String,
1643+
products = (json["products"] as List<*>).map { it as String },
1644+
)
1645+
}
1646+
}
1647+
1648+
fun toJson(): Map<String, Any?> = mapOf(
1649+
"__typename" to "UserChoiceBillingDetails",
1650+
"externalTransactionToken" to externalTransactionToken,
1651+
"products" to products.map { it },
1652+
)
1653+
}
1654+
15361655
public typealias VoidResult = Unit
15371656

15381657
// MARK: - Input Objects
@@ -1795,10 +1914,6 @@ public data class RequestPurchaseIosProps(
17951914
* App account token for user tracking
17961915
*/
17971916
val appAccountToken: String? = null,
1798-
/**
1799-
* External purchase URL for alternative billing (iOS)
1800-
*/
1801-
val externalPurchaseUrl: String? = null,
18021917
/**
18031918
* Purchase quantity
18041919
*/
@@ -1817,7 +1932,6 @@ public data class RequestPurchaseIosProps(
18171932
return RequestPurchaseIosProps(
18181933
andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as Boolean?,
18191934
appAccountToken = json["appAccountToken"] as String?,
1820-
externalPurchaseUrl = json["externalPurchaseUrl"] as String?,
18211935
quantity = (json["quantity"] as Number?)?.toInt(),
18221936
sku = json["sku"] as String,
18231937
withOffer = (json["withOffer"] as Map<String, Any?>?)?.let { DiscountOfferInputIOS.fromJson(it) },
@@ -1828,7 +1942,6 @@ public data class RequestPurchaseIosProps(
18281942
fun toJson(): Map<String, Any?> = mapOf(
18291943
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
18301944
"appAccountToken" to appAccountToken,
1831-
"externalPurchaseUrl" to externalPurchaseUrl,
18321945
"quantity" to quantity,
18331946
"sku" to sku,
18341947
"withOffer" to withOffer?.toJson(),
@@ -1971,10 +2084,6 @@ public data class RequestSubscriptionAndroidProps(
19712084
public data class RequestSubscriptionIosProps(
19722085
val andDangerouslyFinishTransactionAutomatically: Boolean? = null,
19732086
val appAccountToken: String? = null,
1974-
/**
1975-
* External purchase URL for alternative billing (iOS)
1976-
*/
1977-
val externalPurchaseUrl: String? = null,
19782087
val quantity: Int? = null,
19792088
val sku: String,
19802089
val withOffer: DiscountOfferInputIOS? = null
@@ -1984,7 +2093,6 @@ public data class RequestSubscriptionIosProps(
19842093
return RequestSubscriptionIosProps(
19852094
andDangerouslyFinishTransactionAutomatically = json["andDangerouslyFinishTransactionAutomatically"] as Boolean?,
19862095
appAccountToken = json["appAccountToken"] as String?,
1987-
externalPurchaseUrl = json["externalPurchaseUrl"] as String?,
19882096
quantity = (json["quantity"] as Number?)?.toInt(),
19892097
sku = json["sku"] as String,
19902098
withOffer = (json["withOffer"] as Map<String, Any?>?)?.let { DiscountOfferInputIOS.fromJson(it) },
@@ -1995,7 +2103,6 @@ public data class RequestSubscriptionIosProps(
19952103
fun toJson(): Map<String, Any?> = mapOf(
19962104
"andDangerouslyFinishTransactionAutomatically" to andDangerouslyFinishTransactionAutomatically,
19972105
"appAccountToken" to appAccountToken,
1998-
"externalPurchaseUrl" to externalPurchaseUrl,
19992106
"quantity" to quantity,
20002107
"sku" to sku,
20012108
"withOffer" to withOffer?.toJson(),
@@ -2145,6 +2252,14 @@ public interface MutationResolver {
21452252
* Present the App Store code redemption sheet
21462253
*/
21472254
suspend fun presentCodeRedemptionSheetIOS(): Boolean
2255+
/**
2256+
* Present external purchase custom link with StoreKit UI (iOS 18.2+)
2257+
*/
2258+
suspend fun presentExternalPurchaseLinkIOS(url: String): ExternalPurchaseLinkResultIOS
2259+
/**
2260+
* Present external purchase notice sheet (iOS 18.2+)
2261+
*/
2262+
suspend fun presentExternalPurchaseNoticeSheetIOS(): ExternalPurchaseNoticeResultIOS
21482263
/**
21492264
* Initiate a purchase flow; rely on events for final state
21502265
*/
@@ -2184,6 +2299,10 @@ public interface MutationResolver {
21842299
* GraphQL root query operations.
21852300
*/
21862301
public interface QueryResolver {
2302+
/**
2303+
* Check if external purchase notice sheet can be presented (iOS 18.2+)
2304+
*/
2305+
suspend fun canPresentExternalPurchaseNoticeIOS(): Boolean
21872306
/**
21882307
* Get current StoreKit 2 entitlements (iOS 15+)
21892308
*/
@@ -2270,6 +2389,11 @@ public interface SubscriptionResolver {
22702389
* Fires when a purchase completes successfully or a pending purchase resolves
22712390
*/
22722391
suspend fun purchaseUpdated(): Purchase
2392+
/**
2393+
* Fires when a user selects alternative billing in the User Choice Billing dialog (Android only)
2394+
* Only triggered when the user selects alternative billing instead of Google Play billing
2395+
*/
2396+
suspend fun userChoiceBillingAndroid(): UserChoiceBillingDetails
22732397
}
22742398

22752399
// MARK: - Root Operation Helpers
@@ -2287,6 +2411,8 @@ public typealias MutationEndConnectionHandler = suspend () -> Boolean
22872411
public typealias MutationFinishTransactionHandler = suspend (purchase: PurchaseInput, isConsumable: Boolean?) -> Unit
22882412
public typealias MutationInitConnectionHandler = suspend (config: InitConnectionConfig?) -> Boolean
22892413
public typealias MutationPresentCodeRedemptionSheetIOSHandler = suspend () -> Boolean
2414+
public typealias MutationPresentExternalPurchaseLinkIOSHandler = suspend (url: String) -> ExternalPurchaseLinkResultIOS
2415+
public typealias MutationPresentExternalPurchaseNoticeSheetIOSHandler = suspend () -> ExternalPurchaseNoticeResultIOS
22902416
public typealias MutationRequestPurchaseHandler = suspend (params: RequestPurchaseProps) -> RequestPurchaseResult?
22912417
public typealias MutationRequestPurchaseOnPromotedProductIOSHandler = suspend () -> Boolean
22922418
public typealias MutationRestorePurchasesHandler = suspend () -> Unit
@@ -2307,6 +2433,8 @@ public data class MutationHandlers(
23072433
val finishTransaction: MutationFinishTransactionHandler? = null,
23082434
val initConnection: MutationInitConnectionHandler? = null,
23092435
val presentCodeRedemptionSheetIOS: MutationPresentCodeRedemptionSheetIOSHandler? = null,
2436+
val presentExternalPurchaseLinkIOS: MutationPresentExternalPurchaseLinkIOSHandler? = null,
2437+
val presentExternalPurchaseNoticeSheetIOS: MutationPresentExternalPurchaseNoticeSheetIOSHandler? = null,
23102438
val requestPurchase: MutationRequestPurchaseHandler? = null,
23112439
val requestPurchaseOnPromotedProductIOS: MutationRequestPurchaseOnPromotedProductIOSHandler? = null,
23122440
val restorePurchases: MutationRestorePurchasesHandler? = null,
@@ -2318,6 +2446,7 @@ public data class MutationHandlers(
23182446

23192447
// MARK: - Query Helpers
23202448

2449+
public typealias QueryCanPresentExternalPurchaseNoticeIOSHandler = suspend () -> Boolean
23212450
public typealias QueryCurrentEntitlementIOSHandler = suspend (sku: String) -> PurchaseIOS?
23222451
public typealias QueryFetchProductsHandler = suspend (params: ProductRequest) -> FetchProductsResult
23232452
public typealias QueryGetActiveSubscriptionsHandler = suspend (subscriptionIds: List<String>?) -> List<ActiveSubscription>
@@ -2337,6 +2466,7 @@ public typealias QuerySubscriptionStatusIOSHandler = suspend (sku: String) -> Li
23372466
public typealias QueryValidateReceiptIOSHandler = suspend (options: ReceiptValidationProps) -> ReceiptValidationResultIOS
23382467

23392468
public data class QueryHandlers(
2469+
val canPresentExternalPurchaseNoticeIOS: QueryCanPresentExternalPurchaseNoticeIOSHandler? = null,
23402470
val currentEntitlementIOS: QueryCurrentEntitlementIOSHandler? = null,
23412471
val fetchProducts: QueryFetchProductsHandler? = null,
23422472
val getActiveSubscriptions: QueryGetActiveSubscriptionsHandler? = null,
@@ -2361,9 +2491,11 @@ public data class QueryHandlers(
23612491
public typealias SubscriptionPromotedProductIOSHandler = suspend () -> String
23622492
public typealias SubscriptionPurchaseErrorHandler = suspend () -> PurchaseError
23632493
public typealias SubscriptionPurchaseUpdatedHandler = suspend () -> Purchase
2494+
public typealias SubscriptionUserChoiceBillingAndroidHandler = suspend () -> UserChoiceBillingDetails
23642495

23652496
public data class SubscriptionHandlers(
23662497
val promotedProductIOS: SubscriptionPromotedProductIOSHandler? = null,
23672498
val purchaseError: SubscriptionPurchaseErrorHandler? = null,
2368-
val purchaseUpdated: SubscriptionPurchaseUpdatedHandler? = null
2499+
val purchaseUpdated: SubscriptionPurchaseUpdatedHandler? = null,
2500+
val userChoiceBillingAndroid: SubscriptionUserChoiceBillingAndroidHandler? = null
23692501
)

0 commit comments

Comments
 (0)