Skip to content

feat paywall currency fix native store price and usd fallback - #128

Merged
therajanmaurya merged 3 commits into
MobileByteLabs:developmentfrom
therajanmaurya:claude-session-PayCraft-20260727212220
Jul 29, 2026
Merged

feat paywall currency fix native store price and usd fallback#128
therajanmaurya merged 3 commits into
MobileByteLabs:developmentfrom
therajanmaurya:claude-session-PayCraft-20260727212220

Conversation

@therajanmaurya

@therajanmaurya therajanmaurya commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Draft.

Summary by CodeRabbit

  • New Features

    • Added localized pricing from Google Play and Apple App Store storefronts.
    • Improved currency selection using the store region for more accurate paywall prices.
    • Added native StoreKit checkout support for iOS and macOS subscriptions.
    • Added native billing price and storefront support on Android.
  • Bug Fixes

    • Prevented misconfigured native subscriptions from incorrectly falling back to web checkout.
    • Fixed subscription creation requests for App Store Connect.

@vercel

vercel Bot commented Jul 27, 2026

Copy link
Copy Markdown

@therajanmaurya is attempting to deploy a commit to the MobileByteLabs' projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Native storefront country and localized pricing now flow from Play Billing and StoreKit into PayCraft configuration. iOS/macOS digital checkout uses StoreKit with fail-closed routing, native purchases share reconciliation logic, App Store subscription synchronization uses the correct relationship key, and the version is bumped to 2.3.1.

Changes

Native Store Billing

Layer / File(s) Summary
Billing contracts and pricing models
cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/{CurrencyResolver.kt,billing/*,model/ProductPricing.kt}, cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/BillingManager.kt
Adds storefront-aware country resolution, native price data types, StoreKit checkout lanes, native billing APIs, and micros-to-money conversion.
Native storefront and price adapters
cmp-paycraft/src/androidMain/.../PlayBillingNativeClient.android.kt, cmp-paycraft/src/iosMain/.../StoreKit2*, cmp-paycraft/src/iosMain/swift/PayCraftStoreKit2.swift
Reads storefront countries and localized subscription prices from Play Billing and StoreKit.
Storefront-aware configuration and pricing
cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt
Resolves storefront country, fetches native prices by SKU, and reapplies suite configuration with native plan pricing.
Native checkout and purchase reconciliation
cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/{PayCraft.kt,core/PayCraftBillingManager.kt}
Routes Android and Apple digital purchases through native billing, blocks missing configuration, and centralizes entitlement reconciliation.
Native billing and pricing validation
cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/{CurrencyResolverTest.kt,billing/*,core/*,model/*,testsupport/*,ui/*}
Tests storefront precedence, native pricing precedence, StoreKit routing, and fail-closed purchase behavior.

App Store Subscription Synchronization

Layer / File(s) Summary
Subscription group relationship payload
dashboard/lib/appstore-product-sync.ts, dashboard/__tests__/lib/appstore-product-sync.test.ts
Uses relationships.group when creating subscriptions and verifies the request payload and result.

Release Metadata

Layer / File(s) Summary
PayCraft version update
gradle.properties
Updates paycraft.version from 2.3.0 to 2.3.1.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant PayCraft
  participant NativeBillingClient
  participant Store
  participant BillingManager
  PayCraft->>NativeBillingClient: resolve storefront and native price
  NativeBillingClient->>Store: query store configuration and product details
  Store-->>NativeBillingClient: country and localized price
  NativeBillingClient-->>PayCraft: native billing data
  PayCraft->>BillingManager: route native digital checkout
  BillingManager->>Store: purchase product
  Store-->>BillingManager: purchase result
  BillingManager->>BillingManager: reconcile entitlement status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: native store pricing and currency fallback handling.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Fix failing CI checks
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…ytelabs/paycraft/billing/PlayBillingNativeClient.android.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/CurrencyResolver.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt
…illingConfig + ktlint

- PlayBillingNativeClient.android: billing-ktx v8 has no suspend getBillingConfig extension;
  rewrite storefrontCountry() with the callback getBillingConfigAsync wrapped in
  suspendCancellableCoroutine (fixes 'unresolved reference getBillingConfig' + type inference)
- PayCraft.kt: drop leading underscore on two purely-private vars (nativePricesBySku,
  currentSuite) that have no matching public member — ktlint standard:backing-property-naming
  was aborting spotless with an AssertionError
- spotless reformat of touched files

Verified green locally: spotlessCheck, :cmp-paycraft:compileAndroidMain,
:cmp-paycraft:compileKotlinIosSimulatorArm64 (native StoreKit2), :cmp-paycraft:jvmTest.
@therajanmaurya
therajanmaurya marked this pull request as ready for review July 29, 2026 12:22
@therajanmaurya
therajanmaurya merged commit 8c7073e into MobileByteLabs:development Jul 29, 2026
7 of 9 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
dashboard/__tests__/lib/appstore-product-sync.test.ts (1)

39-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore global.fetch after each test.

The direct assignment at Line 39 is never cleaned up, so later tests can inherit this mock. Capture the original value and restore it in afterEach, or use a spy with explicit restoration.

Proposed cleanup
+const originalFetch = (global as unknown as { fetch?: unknown }).fetch
+
 function installFetch() {
   ...
 }
 
 beforeEach(() => jest.clearAllMocks())
+afterEach(() => {
+  ;(global as unknown as { fetch?: unknown }).fetch = originalFetch
+})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@dashboard/__tests__/lib/appstore-product-sync.test.ts` at line 39, Restore
the original global.fetch after each test: capture its value before the mock
assignment in the test setup and add afterEach cleanup that reassigns the
original value. Update the direct fetch assignment near the existing test setup,
preserving the current fetchMock behavior during each test.
cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt (1)

252-301: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider covering the StoreKit success/reconciliation branch.

The guards are well covered, but the newly added register == null success path (direct checkPremiumWithDeviceToken vs refreshStatus(force = true)) has no test — that branch carries the actual behavioral change for StoreKit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt`
around lines 252 - 301, Add a test covering the successful StoreKit purchase
when the registration lookup returns null, exercising purchaseViaStoreKit with a
valid app store product ID and verifying checkPremiumWithDeviceToken is called
directly instead of refreshStatus(force = true). Use the existing fake
service/native client symbols and assert the resulting billing state and
reconciliation behavior.
cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt (1)

120-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused currentSuite cache.

currentSuite is only written in applySuiteConfig, while native-price re-application passes the suite explicitly, so the field provides no value.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt`
around lines 120 - 121, Remove the unused currentSuite property from PayCraft
and delete any assignments to it in applySuiteConfig, since native-price
re-application already receives the SuiteConfig explicitly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt`:
- Around line 273-298: Update the reconciliation flow after the
register/entitlement handling so the BillingState.Error set when register is
non-null and entitlement is null is not overwritten by refreshStatus(force =
true). Track or branch on this verification-failure outcome and skip
reconciliation for it, while preserving the existing direct reconciliation for
Loading and refreshStatus behavior for successful Premium outcomes.

In
`@cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/model/ProductPricing.kt`:
- Around line 25-34: The fromMicros method incorrectly assumes every currency
has two decimal minor units and can overflow through toInt(). Update
ProductPricing’s currency-to-Money conversion to use the currency’s actual
exponent, or preserve micros without fixed scaling, and use a wider amount type
or explicit range validation so large values are not silently truncated.

In `@cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt`:
- Around line 333-337: Update storeProductIdFor to map the "macos" platform to
the App Store/StoreKit product identifier, matching resolveCheckoutLane's
StoreKit routing; preserve the existing Android, iOS, blank-value filtering, and
null fallback behavior.

In `@gradle.properties`:
- Line 17: Align the release version source with the declared paycraft.version
property in gradle.properties. Update scripts/release.sh and the release
workflow/version checks to read and validate that property, ensuring the Git tag
and published artifact consistently use version 2.3.1 rather than a script
default or separately extracted tag version.

---

Nitpick comments:
In `@cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt`:
- Around line 120-121: Remove the unused currentSuite property from PayCraft and
delete any assignments to it in applySuiteConfig, since native-price
re-application already receives the SuiteConfig explicitly.

In
`@cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt`:
- Around line 252-301: Add a test covering the successful StoreKit purchase when
the registration lookup returns null, exercising purchaseViaStoreKit with a
valid app store product ID and verifying checkPremiumWithDeviceToken is called
directly instead of refreshStatus(force = true). Use the existing fake
service/native client symbols and assert the resulting billing state and
reconciliation behavior.

In `@dashboard/__tests__/lib/appstore-product-sync.test.ts`:
- Line 39: Restore the original global.fetch after each test: capture its value
before the mock assignment in the test setup and add afterEach cleanup that
reassigns the original value. Update the direct fetch assignment near the
existing test setup, preserving the current fetchMock behavior during each test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2d7251e7-07ea-46ea-b028-9734cd5fc27f

📥 Commits

Reviewing files that changed from the base of the PR and between c0def04 and 986c760.

📒 Files selected for processing (20)
  • cmp-paycraft/src/androidMain/kotlin/com/mobilebytelabs/paycraft/billing/PlayBillingNativeClient.android.kt
  • cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/CurrencyResolver.kt
  • cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt
  • cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/billing/CheckoutLane.kt
  • cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/billing/NativeBillingClient.kt
  • cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/BillingManager.kt
  • cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt
  • cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/model/ProductPricing.kt
  • cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/CurrencyResolverTest.kt
  • cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/billing/CheckoutRoutingTest.kt
  • cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.kt
  • cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/model/ProductTest.kt
  • cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/testsupport/EntitlementTestSupport.kt
  • cmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftRestoreContentTest.kt
  • cmp-paycraft/src/iosMain/kotlin/com/mobilebytelabs/paycraft/billing/StoreKit2Bridge.kt
  • cmp-paycraft/src/iosMain/kotlin/com/mobilebytelabs/paycraft/billing/StoreKit2NativeBillingClient.ios.kt
  • cmp-paycraft/src/iosMain/swift/PayCraftStoreKit2.swift
  • dashboard/__tests__/lib/appstore-product-sync.test.ts
  • dashboard/lib/appstore-product-sync.ts
  • gradle.properties

Comment on lines +273 to +298
} else if (register != null && entitlement == null) {
// A grant endpoint EXISTS but did not confirm — surface the failure.
_billingState.value = BillingState.Error(
"Purchase completed but could not be verified. Contact support if premium doesn't unlock.",
)
}

// Then reconcile through the normal server path so the entitlement fully lands
// (task 3). refreshStatus(force=true) re-checks server truth for the device.
refreshStatus(force = true)
// Then reconcile through the normal server path so the entitlement fully lands.
if (_billingState.value is BillingState.Loading) {
// register == null (StoreKit): nothing set Premium/Error, so state is still
// Loading. refreshStatus() would skip on its Loading guard — reconcile directly
// instead so ASSN-V2-delivered truth is picked up.
val reconcileEmail = _userEmail.value
if (reconcileEmail != null) {
checkPremiumWithDeviceToken(reconcileEmail)
} else {
_billingState.value = if (_isPremium.value) {
BillingState.Premium(_subscriptionStatus.value)
} else {
BillingState.Free
}
}
} else {
// state is Premium/Error → refreshStatus re-checks server truth for the device.
refreshStatus(force = true)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The "could not be verified" error is immediately overwritten by refreshStatus(force = true).

When register != null and the grant returns null, the branch sets BillingState.Error(...), then control falls into the else branch (state is no longer Loading) and calls refreshStatus(force = true). refreshStatus does not skip on Error, so it sets Free (no email) or LoadingapplyPremiumResult — the user never sees the verification message that this branch exists to surface.

🐛 Proposed fix: don't reconcile over a verification error
                     } else if (register != null && entitlement == null) {
                         // A grant endpoint EXISTS but did not confirm — surface the failure.
                         _billingState.value = BillingState.Error(
                             "Purchase completed but could not be verified. Contact support if premium doesn't unlock.",
                         )
+                        return@launch
                     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (register != null && entitlement == null) {
// A grant endpoint EXISTS but did not confirm — surface the failure.
_billingState.value = BillingState.Error(
"Purchase completed but could not be verified. Contact support if premium doesn't unlock.",
)
}
// Then reconcile through the normal server path so the entitlement fully lands
// (task 3). refreshStatus(force=true) re-checks server truth for the device.
refreshStatus(force = true)
// Then reconcile through the normal server path so the entitlement fully lands.
if (_billingState.value is BillingState.Loading) {
// register == null (StoreKit): nothing set Premium/Error, so state is still
// Loading. refreshStatus() would skip on its Loading guard — reconcile directly
// instead so ASSN-V2-delivered truth is picked up.
val reconcileEmail = _userEmail.value
if (reconcileEmail != null) {
checkPremiumWithDeviceToken(reconcileEmail)
} else {
_billingState.value = if (_isPremium.value) {
BillingState.Premium(_subscriptionStatus.value)
} else {
BillingState.Free
}
}
} else {
// state is Premium/Error → refreshStatus re-checks server truth for the device.
refreshStatus(force = true)
}
} else if (register != null && entitlement == null) {
// A grant endpoint EXISTS but did not confirm — surface the failure.
_billingState.value = BillingState.Error(
"Purchase completed but could not be verified. Contact support if premium doesn't unlock.",
)
return@launch
}
// Then reconcile through the normal server path so the entitlement fully lands.
if (_billingState.value is BillingState.Loading) {
// register == null (StoreKit): nothing set Premium/Error, so state is still
// Loading. refreshStatus() would skip on its Loading guard — reconcile directly
// instead so ASSN-V2-delivered truth is picked up.
val reconcileEmail = _userEmail.value
if (reconcileEmail != null) {
checkPremiumWithDeviceToken(reconcileEmail)
} else {
_billingState.value = if (_isPremium.value) {
BillingState.Premium(_subscriptionStatus.value)
} else {
BillingState.Free
}
}
} else {
// state is Premium/Error → refreshStatus re-checks server truth for the device.
refreshStatus(force = true)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.kt`
around lines 273 - 298, Update the reconciliation flow after the
register/entitlement handling so the BillingState.Error set when register is
non-null and entitlement is null is not overwritten by refreshStatus(force =
true). Track or branch on this verification-failure outcome and skip
reconciliation for it, while preserving the existing direct reconciliation for
Loading and refreshStatus behavior for successful Premium outcomes.

Comment on lines +25 to 34

companion object {
/**
* Build a [Money] from a store price in micro-units (Play `priceAmountMicros` / StoreKit2
* `price` × 1_000_000). Minor units (cents/paise) = micros / 10_000 (1 unit = 100 minor =
* 1_000_000 micros). E.g. ₹799.00 → 799_000_000 micros → 79_900 paise → `Money(79900, "INR")`.
*/
fun fromMicros(micros: Long, currency: String): Money = Money((micros / 10_000L).toInt(), currency)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

fromMicros hard-codes a 2-decimal minor unit, which misprices zero-decimal storefronts.

micros / 10_000 assumes 100 minor units per major unit. For JPY/KRW/CLP/VND storefronts StoreKit/Play report e.g. ¥1,000 as 1_000_000_000 micros → Money(100_000, "JPY"), and Money.format() then renders "JPY 1000.00" only by coincidence of the same divisor — any consumer reading amountMinor as yen gets a 100× value. Consider carrying the currency exponent (or storing micros directly) instead of a fixed 10_000 divisor.

Also note .toInt() silently truncates for high-magnitude currencies (>~21.4M major units).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/model/ProductPricing.kt`
around lines 25 - 34, The fromMicros method incorrectly assumes every currency
has two decimal minor units and can overflow through toInt(). Update
ProductPricing’s currency-to-Money conversion to use the currency’s actual
exponent, or preserve micros without fixed scaling, and use a wider amount type
or explicit range validation so large values are not silently truncated.

Comment on lines +333 to +337
private fun storeProductIdFor(product: ProductDto): String? = when (PlatformInfo.platform.lowercase()) {
"android" -> product.playProductId
"ios" -> product.appStoreProductId
else -> null
}?.takeIf { it.isNotBlank() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

macOS is routed to StoreKit but never gets a native price.

resolveCheckoutLane sends platform == "macos" down the StoreKit lane (CheckoutLane.kt Line 76), yet storeProductIdFor only maps "android" and "ios", so on macOS the paywall keeps the cloud /config price while StoreKit charges the storefront price — the exact mismatch this PR fixes for iOS.

🐛 Proposed fix
     private fun storeProductIdFor(product: ProductDto): String? = when (PlatformInfo.platform.lowercase()) {
         "android" -> product.playProductId
-        "ios" -> product.appStoreProductId
+        "ios", "macos" -> product.appStoreProductId
         else -> null
     }?.takeIf { it.isNotBlank() }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun storeProductIdFor(product: ProductDto): String? = when (PlatformInfo.platform.lowercase()) {
"android" -> product.playProductId
"ios" -> product.appStoreProductId
else -> null
}?.takeIf { it.isNotBlank() }
private fun storeProductIdFor(product: ProductDto): String? = when (PlatformInfo.platform.lowercase()) {
"android" -> product.playProductId
"ios", "macos" -> product.appStoreProductId
else -> null
}?.takeIf { it.isNotBlank() }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt`
around lines 333 - 337, Update storeProductIdFor to map the "macos" platform to
the App Store/StoreKit product identifier, matching resolveCheckoutLane's
StoreKit routing; preserve the existing Android, iOS, blank-value filtering, and
null fallback behavior.

Comment thread gradle.properties
android.nonTransitiveRClass=true
#Publishing
paycraft.version=2.3.0
paycraft.version=2.3.1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

version="$(sed -n 's/^paycraft\.version=//p' gradle.properties)"
rg -n -C3 'paycraft\.version|cmp-paycraft' \
  infra/epic-close-check.sh .github/workflows/publish.yml

if rg -q "^paycraft\\.version=${version}$" infra/epic-close-check.sh; then
  echo "Release gate matches ${version}"
else
  echo "Release gate does not match ${version}" >&2
  exit 1
fi

Repository: MobileByteLabs/PayCraft

Length of output: 2797


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- references to release/gate checks ---\n'
rg -n 'release|G-5|epic-close|cmp-paycraft|paycraft\.version|mavenPublishing|version-source|gradle-properties' \
  .github/workflows gradle.properties infra -S || true

printf '\n--- candidate workflow/stat files ---\n'
git ls-files .github/workflows infra gradle.properties | sort

printf '\n--- version in gradle.properties ---\n'
sed -n '1,80p' gradle.properties

Repository: MobileByteLabs/PayCraft

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- references to release/gate checks ---'
rg -n 'release|G-5|epic-close|cmp-paycraft|paycraft\.version|mavenPublishing|version-source|gradle-properties' \
  .github/workflows gradle.properties infra -S || true

printf '%s\n' ''
printf '%s\n' '--- version in gradle.properties ---'
sed -n '1,80p' gradle.properties

Repository: MobileByteLabs/PayCraft

Length of output: 5137


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- release workflow release job excerpt ---'
sed -n '120,175p' .github/workflows/release.yml

printf '%s\n' ''
printf '%s\n' '--- epic-close script release gating excerpt ---'
sed -n '1,360p' infra/epic-close-check.sh | sed -n '/G-5/,/record "G-5 E2E + Maven"/p'

printf '%s\n' ''
printf '%s\n' '--- scripts that invoke release workflow / epic-close-check.sh ---'
rg -n '\.github/workflows/release\.yml|scripts/release\.sh|epic-close-check\.sh|record "("G-5|run: .*epic-close' .github workflows scripts infra -S || true

Repository: MobileByteLabs/PayCraft

Length of output: 5792


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- files that mention epic-close-check.sh release workflow or scripts/release.sh ---'
rg -n 'epic-close-check\.sh|scripts/release\.sh|\.github/workflows/release\.yml|All gates GREEN|Epic ready to close|Phase fails|G-5 E2E + Maven' \
  scripts .github workflows README.md infra 2>/dev/null -S || true

printf '%s\n' ''
printf '%s\n' '--- repository release-related files ---'
git ls-files 'scripts/**' 'infra/**' '.github/workflows/**' | sort

Repository: MobileByteLabs/PayCraft

Length of output: 3773


Fix the version gate so scripts/release.sh aligns with the declared release version.

scripts/release.sh:10 says it auto-reads the version from build.gradle.kts, but this project declares the release version in gradle.properties and .github/workflows/release.yml extracts it from the Git tag instead. A manual run with this bump would use the script-default/release-tag version rather than 2.3.1, while publish.yml correctly uses gradle-properties-key: 'paycraft.version'. Standardize the flow on the same version source so the tag, published artifact, and check-scripts all match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@gradle.properties` at line 17, Align the release version source with the
declared paycraft.version property in gradle.properties. Update
scripts/release.sh and the release workflow/version checks to read and validate
that property, ensuring the Git tag and published artifact consistently use
version 2.3.1 rather than a script default or separately extracted tag version.

therajanmaurya added a commit that referenced this pull request Aug 2, 2026
* fix(dashboard): surface Google Play + App Store on the Providers list (#123)

The store-sync credential pages (/providers/google-play, /providers/app-store)
shipped in 2.2.0 but were never linked from the Providers index (only web PSPs
render there), so they were unreachable. Adds an 'In-app billing (native app
stores)' section with Connect/Manage cards + live connection status.

* fix(dashboard): surface store sync in products UI (bulk + badges + Re-sync feedback) (#124)

* fix(dashboard): surface Google Play + App Store on the Providers list (#123)

The store-sync credential pages (/providers/google-play, /providers/app-store)
shipped in 2.2.0 but were never linked from the Providers index (only web PSPs
render there), so they were unreachable. Adds an 'In-app billing (native app
stores)' section with Connect/Manage cards + live connection status.

* fix(dashboard): surface store sync in products UI (bulk + badges + Re-sync)

Closes the two store-sync UI gaps from 2.2.0:
- Bulk 'sync-to-providers' now includes Google Play + App Store.
- Products table shows Play / App Store badges; per-product Re-sync panel +
  unsynced banner + sync panel surface store connection/results.
Matches the /sync route snake_case keys (google_play/app_store). tsc clean.

* chore(source): update .github/workflows/deploy-cloud.yml dashboard/app/api/products/[id]/sync/route.ts dashboard/app/api/products/sync-to-providers/route.ts

* chore(source): update dashboard/__tests__/lib/googleplay-product-sync.test.ts dashboard/app/api/products/[id]/sync/route.ts dashboard/app/api/products/sync-to-providers/route.ts

* chore(source): update cmp-paycraft/src/androidMain/kotlin/com/mobilebytelabs/paycraft/PayCraftInitializer.kt cmp-paycraft/src/androidMain/kotlin/com/mobilebytelabs/paycraft/PayCraftPlatform.android.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/di/PayCraftModule.kt (#126)

* chore(release): bump paycraft 2.2.0 → 2.3.0 (commonMain-only native billing) (#127)

* chore(source): update cmp-paycraft/src/androidMain/kotlin/com/mobilebytelabs/paycraft/PayCraftInitializer.kt cmp-paycraft/src/androidMain/kotlin/com/mobilebytelabs/paycraft/PayCraftPlatform.android.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/di/PayCraftModule.kt

* chore(source): update gradle.properties

* feat paywall currency fix native store price and usd fallback (#128)

* chore: initialize session branch — feat paywall currency fix native store price and usd fallback

* chore(source): update cmp-paycraft/src/androidMain/kotlin/com/mobilebytelabs/paycraft/billing/PlayBillingNativeClient.android.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/CurrencyResolver.kt cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.kt

* fix(billing): resolve CI blockers on native-billing PR — Android getBillingConfig + ktlint

- PlayBillingNativeClient.android: billing-ktx v8 has no suspend getBillingConfig extension;
  rewrite storefrontCountry() with the callback getBillingConfigAsync wrapped in
  suspendCancellableCoroutine (fixes 'unresolved reference getBillingConfig' + type inference)
- PayCraft.kt: drop leading underscore on two purely-private vars (nativePricesBySku,
  currentSuite) that have no matching public member — ktlint standard:backing-property-naming
  was aborting spotless with an AssertionError
- spotless reformat of touched files

Verified green locally: spotlessCheck, :cmp-paycraft:compileAndroidMain,
:cmp-paycraft:compileKotlinIosSimulatorArm64 (native StoreKit2), :cmp-paycraft:jvmTest.

* chore: initialize session branch — chore(paycraft): reconcile + close v2 production-readiness epic (OBE by 2.3.x)

* chore(source): update .gitignore CHANGELOG.md cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/CurrencyResolver.kt

* chore(source): update cmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/CountryDetector.kt

* chore: initialize session branch — fix(auth): multi-tenant login callback redirect + ship platform-provider epic dashboard

* chore(source): update dashboard/app/auth/callback/route.ts supabase/.temp/cli-latest

---------

Co-authored-by: MobileByteSensei Community <mobilebytesensei@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant