Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 20 additions & 41 deletions .github/workflows/release-maui.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,15 @@ jobs:
- name: Calculate new version
id: version
run: |
CURRENT_VERSION=$(jq -r '.maui' openiap-versions.json)
CSPROJ=libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj
CURRENT_VERSION=$(sed -n -E 's|.*<PackageVersion>([^<]+)</PackageVersion>.*|\1|p' "$CSPROJ" | head -n 1)
if [ -z "$CURRENT_VERSION" ]; then
CURRENT_VERSION=$(sed -n -E 's|.*<Version>([^<]+)</Version>.*|\1|p' "$CSPROJ" | head -n 1)
fi
if [ -z "$CURRENT_VERSION" ]; then
echo "❌ Unable to read MAUI package version from $CSPROJ"
exit 1
fi
echo "Current version: $CURRENT_VERSION"

VERSION_TYPE="${{ inputs.version }}"
Expand Down Expand Up @@ -186,42 +194,30 @@ jobs:
echo "✓ Tag maui-iap-$VERSION does not exist, proceeding with release"
fi

- name: Update version in openiap-versions.json
if: steps.version.outputs.skip_version_commit != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
jq --arg version "$VERSION" '.maui = $version' openiap-versions.json > openiap-versions.tmp
mv openiap-versions.tmp openiap-versions.json
echo "Updated openiap-versions.json:"
cat openiap-versions.json

- name: Sync version files
if: steps.version.outputs.skip_version_commit != 'true'
run: ./scripts/sync-versions.sh

- name: Update <Version> in OpenIap.Maui.csproj
- name: Update package version in OpenIap.Maui.csproj
if: steps.version.outputs.skip_version_commit != 'true'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
CSPROJ=libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj
if grep -q "<Version>" "$CSPROJ"; then
if grep -q "<PackageVersion>" "$CSPROJ"; then
sed -i '' -E "s|<PackageVersion>[^<]*</PackageVersion>|<PackageVersion>$VERSION</PackageVersion>|" "$CSPROJ"
elif grep -q "<Version>" "$CSPROJ"; then
sed -i '' -E "s|<Version>[^<]*</Version>|<Version>$VERSION</Version>|" "$CSPROJ"
else
# Insert immediately after the first opening <PropertyGroup>.
# Insert immediately after PackageId so package metadata stays grouped.
awk -v ver="$VERSION" '
!inserted && /<PropertyGroup>/ {
!inserted && /<PackageId>/ {
print
print " <Version>" ver "</Version>"
print " <PackageVersion>" ver "</PackageVersion>"
inserted = 1
next
}
{ print }
' "$CSPROJ" > "$CSPROJ.tmp"
mv "$CSPROJ.tmp" "$CSPROJ"
fi
echo "Updated csproj <Version> to $VERSION"
echo "Updated csproj package version to $VERSION"

- name: Commit version updates
if: steps.version.outputs.skip_version_commit != 'true'
Expand All @@ -231,33 +227,16 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

git add openiap-versions.json packages/*/openiap-versions.json libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj
git add libraries/maui-iap/src/OpenIap.Maui/OpenIap.Maui.csproj

if git diff --staged --quiet; then
echo "No version changes to commit"
else
git commit -m "chore(maui): bump version to $VERSION"
for i in 1 2 3; do
if ! git pull --rebase origin main; then
echo "⚠️ Rebase conflict, auto-resolving openiap-versions.json"
for conflict_file in $(git diff --name-only --diff-filter=U); do
if [[ "$conflict_file" == *"openiap-versions.json" ]]; then
git show HEAD:"$conflict_file" > /tmp/theirs.json 2>/dev/null || true
if [ -s /tmp/theirs.json ]; then
jq --arg version "$VERSION" '.maui = $version' /tmp/theirs.json > /tmp/merged.json
cp /tmp/merged.json "$conflict_file"
else
git checkout --theirs "$conflict_file" 2>/dev/null || git checkout --ours "$conflict_file"
fi
git add "$conflict_file"
else
echo "❌ Unexpected conflict in $conflict_file"
exit 1
fi
done
cp openiap-versions.json packages/docs/openiap-versions.json
git add packages/docs/openiap-versions.json
GIT_EDITOR=true git rebase --continue || { echo "❌ Rebase continue failed"; exit 1; }
echo "❌ Rebase conflict while updating MAUI package version"
exit 1
fi
if git push origin main; then
echo "✅ Pushed version update"
Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,11 @@ openiap/
- `libraries/flutter_inapp_purchase/lib/types.dart` - Synced from GQL
- `libraries/godot-iap/addons/godot-iap/types.gd` - Synced from GQL
- `libraries/maui-iap/src/OpenIap.Maui/Types.cs` - Synced from GQL
- `openiap-versions.json` - Managed by CI/CD workflows only
- `openiap-versions.json` - Managed by CI/CD workflows only; tracks only `spec`, `google`, and `apple`

Framework library package versions (React Native, Expo, Flutter, Godot, KMP,
MAUI) live in their own package metadata / release workflows. Do not add
framework-library version keys to `openiap-versions.json`.

Regenerate and sync types:

Expand Down
5 changes: 3 additions & 2 deletions knowledge/internal/04-platform-packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ Version is managed in `openiap-versions.json`:

```json
{
"apple": "1.2.5",
"gql": "1.0.10"
"spec": "2.0.1",
"google": "2.1.3",
"apple": "2.1.6"
}
```

Expand Down
6 changes: 6 additions & 0 deletions knowledge/internal/06-git-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,12 @@ This file is automatically managed by CI/CD workflows during releases:
- GQL releases update `spec` version
- Deploy script (`npm run deploy`) updates `spec` version

The manifest is only for the shared spec and native platform packages:
`spec`, `google`, and `apple`. Framework library package versions
(`react-native-iap`, `expo-iap`, `flutter_inapp_purchase`, `godot-iap`,
`kmp-iap`, `maui-iap`) must stay in each library's own package metadata and
release workflow, not as extra keys in `openiap-versions.json`.

Manual edits will cause version conflicts and deployment issues. Always use the GitHub Actions workflows or deploy script to update versions.

**Why this matters:** If a feature PR sets `apple: "2.1.1"` manually, and then CI auto-bumps on release, CI sees "current is 2.1.1" and bumps to 2.1.2 — skipping 2.1.1 entirely. The published tag becomes 2.1.2 with no 2.1.1 ever existing.
Expand Down
4 changes: 4 additions & 0 deletions libraries/expo-iap/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1215,8 +1215,12 @@ export interface PurchaseCommon {
export interface PurchaseError {
code: ErrorCode;
debugMessage?: (string | null);
isEmptyProductList?: (boolean | null);
message: string;
productId?: (string | null);
productIds?: (string[] | null);
productType?: (string | null);
responseCode?: (number | null);
}

export interface PurchaseIOS extends PurchaseCommon {
Expand Down
16 changes: 16 additions & 0 deletions libraries/flutter_inapp_purchase/lib/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2961,21 +2961,33 @@ class PurchaseError {
const PurchaseError({
required this.code,
this.debugMessage,
this.isEmptyProductList,
required this.message,
this.productId,
this.productIds,
this.productType,
this.responseCode,
});

final ErrorCode code;
final String? debugMessage;
final bool? isEmptyProductList;
final String message;
final String? productId;
final List<String>? productIds;
final String? productType;
final int? responseCode;

factory PurchaseError.fromJson(Map<String, dynamic> json) {
return PurchaseError(
code: ErrorCode.fromJson(json['code'] as String),
debugMessage: json['debugMessage'] as String?,
isEmptyProductList: json['isEmptyProductList'] as bool?,
message: json['message'] as String,
productId: json['productId'] as String?,
productIds: (json['productIds'] as List<dynamic>?) == null ? null : (json['productIds'] as List<dynamic>?)!.map((e) => e as String).toList(),
productType: json['productType'] as String?,
responseCode: json['responseCode'] as int?,
);
}

Expand All @@ -2984,8 +2996,12 @@ class PurchaseError {
'__typename': 'PurchaseError',
'code': code.toJson(),
'debugMessage': debugMessage,
'isEmptyProductList': isEmptyProductList,
'message': message,
'productId': productId,
'productIds': productIds,
'productType': productType,
'responseCode': responseCode,
};
}
}
Expand Down
19 changes: 19 additions & 0 deletions libraries/godot-iap/addons/godot-iap/types.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2287,6 +2287,10 @@ class PurchaseError:
var message: String = ""
var product_id: Variant = null
var debug_message: Variant = null
var response_code: Variant = null
var product_ids: Array[String] = []
var product_type: Variant = null
var is_empty_product_list: Variant = null

static func from_dict(data: Dictionary) -> PurchaseError:
var obj = PurchaseError.new()
Expand All @@ -2302,6 +2306,14 @@ class PurchaseError:
obj.product_id = data["productId"]
if data.has("debugMessage") and data["debugMessage"] != null:
obj.debug_message = data["debugMessage"]
if data.has("responseCode") and data["responseCode"] != null:
obj.response_code = data["responseCode"]
if data.has("productIds") and data["productIds"] != null:
obj.product_ids = data["productIds"]
if data.has("productType") and data["productType"] != null:
obj.product_type = data["productType"]
if data.has("isEmptyProductList") and data["isEmptyProductList"] != null:
obj.is_empty_product_list = data["isEmptyProductList"]
return obj

func to_dict() -> Dictionary:
Expand All @@ -2315,6 +2327,13 @@ class PurchaseError:
dict["productId"] = product_id
if debug_message != null:
dict["debugMessage"] = debug_message
if response_code != null:
dict["responseCode"] = response_code
dict["productIds"] = product_ids
if product_type != null:
dict["productType"] = product_type
if is_empty_product_list != null:
dict["isEmptyProductList"] = is_empty_product_list
return dict

class PurchaseIOS:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3074,17 +3074,25 @@ public data class PurchaseAndroid(
public data class PurchaseError(
val code: ErrorCode,
val debugMessage: String? = null,
val isEmptyProductList: Boolean? = null,
val message: String,
val productId: String? = null
val productId: String? = null,
val productIds: List<String>? = null,
val productType: String? = null,
val responseCode: Int? = null
) {

companion object {
fun fromJson(json: Map<String, Any?>): PurchaseError {
return PurchaseError(
code = (json["code"] as? String)?.let { ErrorCode.fromJson(it) } ?: ErrorCode.Unknown,
debugMessage = json["debugMessage"] as? String,
isEmptyProductList = json["isEmptyProductList"] as? Boolean,
message = json["message"] as? String ?: "",
productId = json["productId"] as? String,
productIds = (json["productIds"] as? List<*>)?.mapNotNull { it as? String },
productType = json["productType"] as? String,
responseCode = (json["responseCode"] as? Number)?.toInt(),
)
}
}
Expand All @@ -3093,8 +3101,12 @@ public data class PurchaseError(
"__typename" to "PurchaseError",
"code" to code.toJson(),
"debugMessage" to debugMessage,
"isEmptyProductList" to isEmptyProductList,
"message" to message,
"productId" to productId,
"productIds" to productIds,
"productType" to productType,
"responseCode" to responseCode,
)
}

Expand Down
2 changes: 1 addition & 1 deletion libraries/maui-iap/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ libraries/maui-iap/
├── README.md — public-facing intro, install
├── CLAUDE.md — this file
├── CONVENTION.md — C# / MAUI conventions
├── openiap-versions.json — symlink to repo-root version manifest
├── openiap-versions.json — symlink for native spec/apple/google versions
└── src/
└── OpenIap.Maui/
├── OpenIap.Maui.csproj — multi-target (net9.0 + ios/android/maccatalyst)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,7 @@ class OpenIapMauiModule(context: Context) {
* generated C# record expects.
*/
private fun encodeError(e: OpenIapError): Map<String, Any?> {
val diagnostics = e.toJSON()
val productId = when (e) {
is OpenIapError.ProductNotFound -> e.productId
else -> null
Expand All @@ -411,6 +412,10 @@ class OpenIapMauiModule(context: Context) {
"message" to e.message,
"productId" to productId,
"debugMessage" to e.debugMessage,
"responseCode" to diagnostics["responseCode"],
"productIds" to diagnostics["productIds"],
"productType" to diagnostics["productType"],
"isEmptyProductList" to diagnostics["isEmptyProductList"],
)
Comment thread
hyochan marked this conversation as resolved.
}

Expand Down
8 changes: 8 additions & 0 deletions libraries/maui-iap/src/OpenIap.Maui/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3121,10 +3121,18 @@ public sealed record PurchaseError
public required ErrorCode Code { get; init; }
[JsonPropertyName("debugMessage")]
public string? DebugMessage { get; init; }
[JsonPropertyName("isEmptyProductList")]
public bool? IsEmptyProductList { get; init; }
[JsonPropertyName("message")]
public required string Message { get; init; }
[JsonPropertyName("productId")]
public string? ProductId { get; init; }
[JsonPropertyName("productIds")]
public IReadOnlyList<string>? ProductIds { get; init; }
[JsonPropertyName("productType")]
public string? ProductType { get; init; }
[JsonPropertyName("responseCode")]
public int? ResponseCode { get; init; }
}

public sealed record PurchaseIOS : Purchase, PurchaseCommon
Expand Down
Loading
Loading