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

Commit 8336ff3

Browse files
committed
feat: support alternative billing
1 parent 2e0b9ff commit 8336ff3

14 files changed

+494
-28
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ generators/dart/.dart_tool/
44
generators/dart/build/
55
generators/dart/lib/generated/
66
generators/swift/Generated/
7+
.claude

scripts/fix-generated-types.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,15 @@ content = content.replace(
204204
' /** Per-platform purchase request props */',
205205
' request: RequestPurchasePropsByPlatforms;',
206206
" type: 'in-app';",
207+
' /** Use alternative billing (Google Play alternative billing, Apple external purchase link) */',
208+
' useAlternativeBilling?: boolean | null;',
207209
' }',
208210
' | {',
209211
' /** Per-platform subscription request props */',
210212
' request: RequestSubscriptionPropsByPlatforms;',
211213
" type: 'subs';",
214+
' /** Use alternative billing (Google Play alternative billing, Apple external purchase link) */',
215+
' useAlternativeBilling?: boolean | null;',
212216
' };\n\n',
213217
].join('\n'),
214218
);
@@ -221,11 +225,15 @@ content = content.replace(
221225
' /** Per-platform purchase request props */',
222226
' request: RequestPurchasePropsByPlatforms;',
223227
" type: 'in-app';",
228+
' /** Use alternative billing (Google Play alternative billing, Apple external purchase link) */',
229+
' useAlternativeBilling?: boolean | null;',
224230
' }',
225231
' | {',
226232
' /** Per-platform subscription request props */',
227233
' request: RequestSubscriptionPropsByPlatforms;',
228234
" type: 'subs';",
235+
' /** Use alternative billing (Google Play alternative billing, Apple external purchase link) */',
236+
' useAlternativeBilling?: boolean | null;',
229237
' };\n\n',
230238
].join('\n'),
231239
);

scripts/generate-dart-types.mjs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -478,29 +478,48 @@ const printObject = (objectType) => {
478478
lines.push(`class ${objectType.name}${extendsClause}${implementsClause} {`);
479479
lines.push(` const ${objectType.name}({`);
480480
const fields = Object.values(objectType.getFields()).sort((a, b) => a.name.localeCompare(b.name));
481+
482+
// Special handling for PurchaseAndroid and PurchaseIOS to add isAlternativeBilling if missing
483+
const needsAlternativeBilling = (objectType.name === 'PurchaseAndroid' || objectType.name === 'PurchaseIOS')
484+
&& !fields.some(f => f.name === 'isAlternativeBilling');
485+
481486
const fieldInfos = fields.map((field) => {
482487
const { type, nullable, metadata } = getDartType(field.type);
483488
const fieldName = escapeDartName(field.name);
484489
return { field, fieldName, type, nullable, metadata };
485490
});
491+
486492
fieldInfos.forEach(({ field, nullable, fieldName }) => {
487493
addDocComment(lines, field.description, ' ');
488494
const line = ` ${nullable ? '' : 'required '}this.${fieldName},`;
489495
lines.push(line);
490496
});
497+
498+
if (needsAlternativeBilling) {
499+
lines.push(' this.isAlternativeBilling,');
500+
}
501+
491502
lines.push(' });', '');
492503
fieldInfos.forEach(({ field, type, nullable, fieldName }) => {
493504
addDocComment(lines, field.description, ' ');
494505
const fieldType = `${type}${nullable ? '?' : ''}`;
495506
lines.push(` final ${fieldType} ${fieldName};`);
496507
});
508+
509+
if (needsAlternativeBilling) {
510+
lines.push(' final bool? isAlternativeBilling;');
511+
}
512+
497513
lines.push('');
498514
lines.push(` factory ${objectType.name}.fromJson(Map<String, dynamic> json) {`);
499515
lines.push(` return ${objectType.name}(`);
500516
fieldInfos.forEach(({ field, fieldName, metadata }) => {
501517
const jsonExpression = buildFromJsonExpression(metadata, `json['${field.name}']`);
502518
lines.push(` ${fieldName}: ${jsonExpression},`);
503519
});
520+
if (needsAlternativeBilling) {
521+
lines.push(` isAlternativeBilling: json['isAlternativeBilling'] as bool?,`);
522+
}
504523
lines.push(' );');
505524
lines.push(' }', '');
506525
if (baseUnion) {
@@ -513,6 +532,9 @@ const printObject = (objectType) => {
513532
const toJsonExpression = buildToJsonExpression(metadata, fieldName);
514533
lines.push(` '${field.name}': ${toJsonExpression},`);
515534
});
535+
if (needsAlternativeBilling) {
536+
lines.push(` 'isAlternativeBilling': isAlternativeBilling,`);
537+
}
516538
lines.push(' };');
517539
lines.push(' }');
518540
lines.push('}', '');
@@ -525,6 +547,7 @@ const printInput = (inputType) => {
525547
lines.push(' RequestPurchaseProps({');
526548
lines.push(' required this.request,');
527549
lines.push(' ProductQueryType? type,');
550+
lines.push(' this.useAlternativeBilling,');
528551
lines.push(' }) : type = type ?? (request is RequestPurchasePropsRequestPurchase');
529552
lines.push(' ? ProductQueryType.InApp');
530553
lines.push(' : ProductQueryType.Subs) {');
@@ -538,18 +561,20 @@ const printInput = (inputType) => {
538561
lines.push('');
539562
lines.push(' final RequestPurchasePropsRequest request;');
540563
lines.push(' final ProductQueryType type;');
564+
lines.push(' final bool? useAlternativeBilling;');
541565
lines.push('');
542566
lines.push(' factory RequestPurchaseProps.fromJson(Map<String, dynamic> json) {');
543567
lines.push(" final typeValue = json['type'] as String?;");
544568
lines.push(' final parsedType = typeValue != null ? ProductQueryType.fromJson(typeValue) : null;');
569+
lines.push(" final useAlternativeBilling = json['useAlternativeBilling'] as bool?;");
545570
lines.push(" final purchaseJson = json['requestPurchase'] as Map<String, dynamic>?;");
546571
lines.push(' if (purchaseJson != null) {');
547572
lines.push(' final request = RequestPurchasePropsRequestPurchase(RequestPurchasePropsByPlatforms.fromJson(purchaseJson));');
548573
lines.push(' final finalType = parsedType ?? ProductQueryType.InApp;');
549574
lines.push(' if (finalType != ProductQueryType.InApp) {');
550575
lines.push(" throw ArgumentError('type must be IN_APP when requestPurchase is provided');");
551576
lines.push(' }');
552-
lines.push(' return RequestPurchaseProps(request: request, type: finalType);');
577+
lines.push(' return RequestPurchaseProps(request: request, type: finalType, useAlternativeBilling: useAlternativeBilling);');
553578
lines.push(' }');
554579
lines.push(" final subscriptionJson = json['requestSubscription'] as Map<String, dynamic>?;");
555580
lines.push(' if (subscriptionJson != null) {');
@@ -558,7 +583,7 @@ const printInput = (inputType) => {
558583
lines.push(' if (finalType != ProductQueryType.Subs) {');
559584
lines.push(" throw ArgumentError('type must be SUBS when requestSubscription is provided');");
560585
lines.push(' }');
561-
lines.push(' return RequestPurchaseProps(request: request, type: finalType);');
586+
lines.push(' return RequestPurchaseProps(request: request, type: finalType, useAlternativeBilling: useAlternativeBilling);');
562587
lines.push(' }');
563588
lines.push(" throw ArgumentError('RequestPurchaseProps requires requestPurchase or requestSubscription');");
564589
lines.push(' }');
@@ -568,23 +593,25 @@ const printInput = (inputType) => {
568593
lines.push(' return {');
569594
lines.push(" 'requestPurchase': (request as RequestPurchasePropsRequestPurchase).value.toJson(),");
570595
lines.push(" 'type': type.toJson(),");
596+
lines.push(" 'useAlternativeBilling': useAlternativeBilling,");
571597
lines.push(' };');
572598
lines.push(' }');
573599
lines.push(' if (request is RequestPurchasePropsRequestSubscription) {');
574600
lines.push(' return {');
575601
lines.push(" 'requestSubscription': (request as RequestPurchasePropsRequestSubscription).value.toJson(),");
576602
lines.push(" 'type': type.toJson(),");
603+
lines.push(" 'useAlternativeBilling': useAlternativeBilling,");
577604
lines.push(' };');
578605
lines.push(' }');
579606
lines.push(" throw StateError('Unsupported RequestPurchaseProps request variant');");
580607
lines.push(' }');
581608
lines.push('');
582-
lines.push(' static RequestPurchaseProps inApp({required RequestPurchasePropsByPlatforms request}) {');
583-
lines.push(' return RequestPurchaseProps(request: RequestPurchasePropsRequestPurchase(request), type: ProductQueryType.InApp);');
609+
lines.push(' static RequestPurchaseProps inApp({required RequestPurchasePropsByPlatforms request, bool? useAlternativeBilling}) {');
610+
lines.push(' return RequestPurchaseProps(request: RequestPurchasePropsRequestPurchase(request), type: ProductQueryType.InApp, useAlternativeBilling: useAlternativeBilling);');
584611
lines.push(' }');
585612
lines.push('');
586-
lines.push(' static RequestPurchaseProps subs({required RequestSubscriptionPropsByPlatforms request}) {');
587-
lines.push(' return RequestPurchaseProps(request: RequestPurchasePropsRequestSubscription(request), type: ProductQueryType.Subs);');
613+
lines.push(' static RequestPurchaseProps subs({required RequestSubscriptionPropsByPlatforms request, bool? useAlternativeBilling}) {');
614+
lines.push(' return RequestPurchaseProps(request: RequestPurchasePropsRequestSubscription(request), type: ProductQueryType.Subs, useAlternativeBilling: useAlternativeBilling);');
588615
lines.push(' }');
589616
lines.push('}');
590617
lines.push('');

scripts/generate-kotlin-types.mjs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,8 @@ const printInput = (inputType) => {
521521
addDocComment(lines, inputType.description);
522522
lines.push('public data class RequestPurchaseProps(');
523523
lines.push(' val request: Request,');
524-
lines.push(' val type: ProductQueryType');
524+
lines.push(' val type: ProductQueryType,');
525+
lines.push(' val useAlternativeBilling: Boolean? = null');
525526
lines.push(') {');
526527
lines.push(' init {');
527528
lines.push(' when (request) {');
@@ -533,19 +534,20 @@ const printInput = (inputType) => {
533534
lines.push(' companion object {');
534535
lines.push(' fun fromJson(json: Map<String, Any?>): RequestPurchaseProps {');
535536
lines.push(' val rawType = (json["type"] as String?)?.let { ProductQueryType.fromJson(it) }');
537+
lines.push(' val useAlternativeBilling = json["useAlternativeBilling"] as Boolean?');
536538
lines.push(' val purchaseJson = json["requestPurchase"] as Map<String, Any?>?');
537539
lines.push(' if (purchaseJson != null) {');
538540
lines.push(' val request = Request.Purchase(RequestPurchasePropsByPlatforms.fromJson(purchaseJson))');
539541
lines.push(' val finalType = rawType ?: ProductQueryType.InApp');
540542
lines.push(' require(finalType == ProductQueryType.InApp) { "type must be IN_APP when requestPurchase is provided" }');
541-
lines.push(' return RequestPurchaseProps(request = request, type = finalType)');
543+
lines.push(' return RequestPurchaseProps(request = request, type = finalType, useAlternativeBilling = useAlternativeBilling)');
542544
lines.push(' }');
543545
lines.push(' val subscriptionJson = json["requestSubscription"] as Map<String, Any?>?');
544546
lines.push(' if (subscriptionJson != null) {');
545547
lines.push(' val request = Request.Subscription(RequestSubscriptionPropsByPlatforms.fromJson(subscriptionJson))');
546548
lines.push(' val finalType = rawType ?: ProductQueryType.Subs');
547549
lines.push(' require(finalType == ProductQueryType.Subs) { "type must be SUBS when requestSubscription is provided" }');
548-
lines.push(' return RequestPurchaseProps(request = request, type = finalType)');
550+
lines.push(' return RequestPurchaseProps(request = request, type = finalType, useAlternativeBilling = useAlternativeBilling)');
549551
lines.push(' }');
550552
lines.push(' throw IllegalArgumentException("RequestPurchaseProps requires requestPurchase or requestSubscription")');
551553
lines.push(' }');
@@ -555,10 +557,12 @@ const printInput = (inputType) => {
555557
lines.push(' is Request.Purchase -> mapOf(');
556558
lines.push(' "requestPurchase" to request.value.toJson(),');
557559
lines.push(' "type" to type.toJson(),');
560+
lines.push(' "useAlternativeBilling" to useAlternativeBilling,');
558561
lines.push(' )');
559562
lines.push(' is Request.Subscription -> mapOf(');
560563
lines.push(' "requestSubscription" to request.value.toJson(),');
561564
lines.push(' "type" to type.toJson(),');
565+
lines.push(' "useAlternativeBilling" to useAlternativeBilling,');
562566
lines.push(' )');
563567
lines.push(' }');
564568
lines.push('');

scripts/generate-swift-types.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,9 @@ const printInput = (inputType) => {
365365
lines.push('public struct RequestPurchaseProps: Codable {');
366366
lines.push(' public var request: Request');
367367
lines.push(' public var type: ProductQueryType');
368+
lines.push(' public var useAlternativeBilling: Bool?');
368369
lines.push('');
369-
lines.push(' public init(request: Request, type: ProductQueryType? = nil) {');
370+
lines.push(' public init(request: Request, type: ProductQueryType? = nil, useAlternativeBilling: Bool? = nil) {');
370371
lines.push(' switch request {');
371372
lines.push(' case .purchase:');
372373
lines.push(' let resolved = type ?? .inApp');
@@ -378,17 +379,20 @@ const printInput = (inputType) => {
378379
lines.push(' self.type = resolved');
379380
lines.push(' }');
380381
lines.push(' self.request = request');
382+
lines.push(' self.useAlternativeBilling = useAlternativeBilling');
381383
lines.push(' }');
382384
lines.push('');
383385
lines.push(' private enum CodingKeys: String, CodingKey {');
384386
lines.push(' case requestPurchase');
385387
lines.push(' case requestSubscription');
386388
lines.push(' case type');
389+
lines.push(' case useAlternativeBilling');
387390
lines.push(' }');
388391
lines.push('');
389392
lines.push(' public init(from decoder: Decoder) throws {');
390393
lines.push(' let container = try decoder.container(keyedBy: CodingKeys.self)');
391394
lines.push(' let decodedType = try container.decodeIfPresent(ProductQueryType.self, forKey: .type)');
395+
lines.push(' self.useAlternativeBilling = try container.decodeIfPresent(Bool.self, forKey: .useAlternativeBilling)');
392396
lines.push(' if let purchase = try container.decodeIfPresent(RequestPurchasePropsByPlatforms.self, forKey: .requestPurchase) {');
393397
lines.push(' let finalType = decodedType ?? .inApp');
394398
lines.push(' guard finalType == .inApp else {');
@@ -419,6 +423,7 @@ const printInput = (inputType) => {
419423
lines.push(' try container.encode(value, forKey: .requestSubscription)');
420424
lines.push(' }');
421425
lines.push(' try container.encode(type, forKey: .type)');
426+
lines.push(' try container.encodeIfPresent(useAlternativeBilling, forKey: .useAlternativeBilling)');
422427
lines.push(' }');
423428
lines.push('');
424429
lines.push(' public enum Request {');

src/api-android.graphql

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,36 @@ extend type Mutation {
1111
"""
1212
# Future
1313
consumePurchaseAndroid(purchaseToken: String!): Boolean!
14+
15+
# Alternative Billing APIs
16+
"""
17+
Check if alternative billing is available for this user/device
18+
Step 1 of alternative billing flow
19+
20+
Returns true if available, false otherwise
21+
Throws OpenIapError.NotPrepared if billing client not ready
22+
"""
23+
# Future
24+
checkAlternativeBillingAvailabilityAndroid: Boolean!
25+
"""
26+
Show alternative billing information dialog to user
27+
Step 2 of alternative billing flow
28+
Must be called BEFORE processing payment in your payment system
29+
30+
Returns true if user accepted, false if user canceled
31+
Throws OpenIapError.NotPrepared if billing client not ready
32+
"""
33+
# Future
34+
showAlternativeBillingDialogAndroid: Boolean!
35+
"""
36+
Create external transaction token for Google Play reporting
37+
Step 3 of alternative billing flow
38+
Must be called AFTER successful payment in your payment system
39+
Token must be reported to Google Play backend within 24 hours
40+
41+
Returns token string, or null if creation failed
42+
Throws OpenIapError.NotPrepared if billing client not ready
43+
"""
44+
# Future
45+
createAlternativeBillingTokenAndroid: String
1446
}

src/api.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ extend type Mutation {
3535
Establish the platform billing connection
3636
"""
3737
# Future
38-
initConnection: Boolean!
38+
initConnection(config: InitConnectionConfig): Boolean!
3939
"""
4040
Close the platform billing connection
4141
"""

0 commit comments

Comments
 (0)