feat paywall currency fix native store price and usd fallback - #128
Conversation
…tore price and usd fallback
|
@therajanmaurya is attempting to deploy a commit to the MobileByteLabs' projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughNative 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. ChangesNative Store Billing
App Store Subscription Synchronization
Release Metadata
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
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
…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.
8c7073e
into
MobileByteLabs:development
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
dashboard/__tests__/lib/appstore-product-sync.test.ts (1)
39-39: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winRestore
global.fetchafter 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 winConsider covering the StoreKit success/reconciliation branch.
The guards are well covered, but the newly added
register == nullsuccess path (directcheckPremiumWithDeviceTokenvsrefreshStatus(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 valueRemove the unused
currentSuitecache.
currentSuiteis only written inapplySuiteConfig, 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
📒 Files selected for processing (20)
cmp-paycraft/src/androidMain/kotlin/com/mobilebytelabs/paycraft/billing/PlayBillingNativeClient.android.ktcmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/CurrencyResolver.ktcmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/PayCraft.ktcmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/billing/CheckoutLane.ktcmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/billing/NativeBillingClient.ktcmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/BillingManager.ktcmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManager.ktcmp-paycraft/src/commonMain/kotlin/com/mobilebytelabs/paycraft/model/ProductPricing.ktcmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/CurrencyResolverTest.ktcmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/billing/CheckoutRoutingTest.ktcmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/core/PayCraftBillingManagerTest.ktcmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/model/ProductTest.ktcmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/testsupport/EntitlementTestSupport.ktcmp-paycraft/src/commonTest/kotlin/com/mobilebytelabs/paycraft/ui/PayCraftRestoreContentTest.ktcmp-paycraft/src/iosMain/kotlin/com/mobilebytelabs/paycraft/billing/StoreKit2Bridge.ktcmp-paycraft/src/iosMain/kotlin/com/mobilebytelabs/paycraft/billing/StoreKit2NativeBillingClient.ios.ktcmp-paycraft/src/iosMain/swift/PayCraftStoreKit2.swiftdashboard/__tests__/lib/appstore-product-sync.test.tsdashboard/lib/appstore-product-sync.tsgradle.properties
| } 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) | ||
| } |
There was a problem hiding this comment.
🎯 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 Loading → applyPremiumResult — 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.
| } 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.
|
|
||
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 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.
| private fun storeProductIdFor(product: ProductDto): String? = when (PlatformInfo.platform.lowercase()) { | ||
| "android" -> product.playProductId | ||
| "ios" -> product.appStoreProductId | ||
| else -> null | ||
| }?.takeIf { it.isNotBlank() } |
There was a problem hiding this comment.
🎯 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.
| 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.
| android.nonTransitiveRClass=true | ||
| #Publishing | ||
| paycraft.version=2.3.0 | ||
| paycraft.version=2.3.1 |
There was a problem hiding this comment.
🗄️ 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
fiRepository: 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.propertiesRepository: 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.propertiesRepository: 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 || trueRepository: 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/**' | sortRepository: 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.
* 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>
Draft.
Summary by CodeRabbit
New Features
Bug Fixes