Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/badges/branches.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ The changelog for `Superwall`. Also see the [releases](https://github.com/superw

## Unreleased

- Web purchase redemption now exposes the full checkout product in `didRedeemLink`, tracks `freeTrial_start` once per code, and schedules the active paywall's trial reminders from the checkout timestamp. Notification permission waits no longer block access or drop a late grant; ambiguous or already-elapsed reminders are skipped.
- Fix multi-page paywalls only reporting the entry page view. `paywall_open` now waits for an in-flight `template_variables` send, so the runtime does not treat a late template payload as a fresh load and drop later `page_view`s.
- Fix an active paywall not being reopened after its webview process crashes and is recreated. Recovery cancels template work for the old document and sends the open after the replacement loads, only if the same presentation is still active.
- Fix prices not showing when product/offers are fetched from cache
Expand Down
4 changes: 4 additions & 0 deletions superwall/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ android {
buildConfig = true
}

testOptions {
unitTests.isIncludeAndroidResources = true
}

kotlinOptions {
jvmTarget = "17"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import com.superwall.sdk.models.entitlements.SubscriptionStatus
import com.superwall.sdk.models.entitlements.TransactionReceipt
import com.superwall.sdk.models.events.EventData
import com.superwall.sdk.models.internal.VendorId
import com.superwall.sdk.models.paywall.LocalNotification
import com.superwall.sdk.models.paywall.LocalNotificationType
import com.superwall.sdk.models.paywall.Paywall
import com.superwall.sdk.models.product.ProductVariable
Expand Down Expand Up @@ -132,10 +133,12 @@ import com.superwall.sdk.utilities.ErrorTracker
import com.superwall.sdk.utilities.dateFormat
import com.superwall.sdk.web.DeepLinkReferrer
import com.superwall.sdk.web.WebPaywallRedeemer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import com.superwall.sdk.models.serialization.DateSerializer
import kotlinx.serialization.json.ClassDiscriminatorMode
import kotlinx.serialization.json.Json
Expand Down Expand Up @@ -1308,6 +1311,24 @@ class DependencyContainer(
}
}

override suspend fun scheduleTrialNotifications(notifications: List<LocalNotification>) {
withContext(Dispatchers.Main.immediate) {
val paywallView = Superwall.instance.paywallView ?: return@withContext
val activity =
(paywallView.encapsulatingActivity?.get() ?: activityProvider?.getCurrentActivity())
as? SuperwallPaywallActivity ?: return@withContext
if (!activity.isFinishing && !activity.isDestroyed) {
// Web reminders already use an absolute checkout timestamp, including in sandbox.
activity.attemptToScheduleNotifications(
notifications,
this@DependencyContainer,
cancelExisting = false,
applySandboxScaling = false,
)
}
}
}

override fun isPaymentSheetOpen(): Boolean {
// TODO: Track payment sheet state
return false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,20 +125,146 @@ sealed class RedemptionResult {
) : RedemptionResult()

@Serializable
data class PaywallInfo(
@SerialName("identifier")
val identifier: PaywallIdentifier,
@SerialName("placementName")
val placementName: String,
@SerialName("placementParams")
val placementParams: Map<String, JsonElement>,
@SerialName("variantId")
val variantId: VariantId,
@SerialName("experimentId")
val experimentId: ExperimentId,
@SerialName("productIdentifier")
val productIdentifier: String? = null,
)
class PaywallInfo
@JvmOverloads
Comment thread
pullfrog[bot] marked this conversation as resolved.
Comment thread
pullfrog[bot] marked this conversation as resolved.
constructor(
@SerialName("identifier") val identifier: PaywallIdentifier,
@SerialName("placementName") val placementName: String,
@SerialName("placementParams") val placementParams: Map<String, JsonElement>,
@SerialName("variantId") val variantId: VariantId,
@SerialName("experimentId") val experimentId: ExperimentId,
@SerialName("productIdentifier") val productIdentifier: String? = null,
) {
/** Original checkout variables. Kept outside the constructor to preserve the Kotlin JVM ABI. */
@SerialName("product")
var product: PaywallProduct? = null
private set

constructor(
identifier: PaywallIdentifier,
placementName: String,
placementParams: Map<String, JsonElement>,
variantId: VariantId,
experimentId: ExperimentId,
productIdentifier: String? = null,
product: PaywallProduct?,
) : this(identifier, placementName, placementParams, variantId, experimentId, productIdentifier) {
this.product = product
}

// Retain the original copy/copy$default and component signatures for precompiled Kotlin callers.
fun copy(
identifier: PaywallIdentifier = this.identifier,
placementName: String = this.placementName,
placementParams: Map<String, JsonElement> = this.placementParams,
variantId: VariantId = this.variantId,
experimentId: ExperimentId = this.experimentId,
productIdentifier: String? = this.productIdentifier,
): PaywallInfo = PaywallInfo(identifier, placementName, placementParams, variantId, experimentId, productIdentifier, product)

fun copy(
identifier: PaywallIdentifier = this.identifier,
placementName: String = this.placementName,
placementParams: Map<String, JsonElement> = this.placementParams,
variantId: VariantId = this.variantId,
experimentId: ExperimentId = this.experimentId,
productIdentifier: String? = this.productIdentifier,
product: PaywallProduct?,
): PaywallInfo = PaywallInfo(identifier, placementName, placementParams, variantId, experimentId, productIdentifier, product)

operator fun component1(): PaywallIdentifier = identifier

operator fun component2(): String = placementName

operator fun component3(): Map<String, JsonElement> = placementParams

operator fun component4(): VariantId = variantId

operator fun component5(): ExperimentId = experimentId

operator fun component6(): String? = productIdentifier

operator fun component7(): PaywallProduct? = product

override fun equals(other: Any?): Boolean =
other is PaywallInfo &&
identifier == other.identifier && placementName == other.placementName &&
placementParams == other.placementParams && variantId == other.variantId &&
experimentId == other.experimentId && productIdentifier == other.productIdentifier && product == other.product

override fun hashCode(): Int =
listOf(identifier, placementName, placementParams, variantId, experimentId, productIdentifier, product).hashCode()

override fun toString(): String =
"PaywallInfo(identifier=$identifier, placementName=$placementName, placementParams=$placementParams, " +
"variantId=$variantId, experimentId=$experimentId, productIdentifier=$productIdentifier, product=$product)"

@Serializable
data class PaywallProduct(
@SerialName("identifier")
val identifier: String,
@SerialName("languageCode")
val languageCode: String = "",
@SerialName("locale")
val locale: String = "",
@SerialName("currencyCode")
val currencyCode: String = "",
@SerialName("currencySymbol")
val currencySymbol: String = "",
@SerialName("period")
val period: String = "",
@SerialName("periodly")
val periodly: String = "",
@SerialName("localizedPeriod")
val localizedPeriod: String = "",
@SerialName("periodAlt")
val periodAlt: String = "",
@SerialName("periodDays")
val periodDays: Int = 0,
@SerialName("periodWeeks")
val periodWeeks: Int = 0,
@SerialName("periodMonths")
val periodMonths: Int = 0,
@SerialName("periodYears")
val periodYears: Int = 0,
@SerialName("rawPrice")
val rawPrice: Double = 0.0,
@SerialName("price")
val price: String = "",
@SerialName("dailyPrice")
val dailyPrice: String = "",
@SerialName("weeklyPrice")
val weeklyPrice: String = "",
@SerialName("monthlyPrice")
val monthlyPrice: String = "",
@SerialName("yearlyPrice")
val yearlyPrice: String = "",
@SerialName("rawTrialPeriodPrice")
val rawTrialPeriodPrice: Double = 0.0,
@SerialName("trialPeriodPrice")
val trialPeriodPrice: String = "",
@SerialName("trialPeriodDailyPrice")
val trialPeriodDailyPrice: String = "",
@SerialName("trialPeriodWeeklyPrice")
val trialPeriodWeeklyPrice: String = "",
@SerialName("trialPeriodMonthlyPrice")
val trialPeriodMonthlyPrice: String = "",
@SerialName("trialPeriodYearlyPrice")
val trialPeriodYearlyPrice: String = "",
@SerialName("trialPeriodDays")
val trialPeriodDays: Int = 0,
@SerialName("trialPeriodWeeks")
val trialPeriodWeeks: Int = 0,
@SerialName("trialPeriodMonths")
val trialPeriodMonths: Int = 0,
@SerialName("trialPeriodYears")
val trialPeriodYears: Int = 0,
@SerialName("trialPeriodText")
val trialPeriodText: String = "",
@SerialName("trialPeriodEndDate")
val trialPeriodEndDate: String = "",
)
}
}

@Serializable
Expand Down
Loading
Loading