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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

The changelog for `Superwall`. Also see the [releases](https://github.com/superwall/Superwall-Android/releases) on GitHub.

## 2.7.0

## 2.6.9
### Enhancements
- Enables paywall post-purchase action execution instead of dismissing

## Deprecations
- Deprecated `paywallWebviewLoad_timeout` - this event was causing confusion due to it's naming, leading to it being deprecated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ class PaywallMessageParsingTest {
// region Purchase

@Test
fun parse_purchase_returns_Purchase_with_product_info() {
Given("a JSON with purchase event") {
fun parse_purchase_returns_Purchase_with_product_info_and_default_shouldDismiss() {
Given("a JSON with purchase event without should_dismiss") {
val json =
"""
{
Expand All @@ -360,13 +360,86 @@ class PaywallMessageParsingTest {
When("parsed") {
val result = parseWrappedPaywallMessages(json)

Then("it returns Purchase with correct product and productId") {
Then("it returns Purchase with correct product, productId, and shouldDismiss defaults to false") {
assertTrue(result.isSuccess)
val messages = result.getOrThrow().payload.messages
assertEquals(1, messages.size)
val message = messages[0] as PaywallMessage.Purchase
assertEquals("primary", message.product)
assertEquals("com.app.subscription.monthly", message.productId)
assertEquals(true, message.shouldDismiss)
}
}
}
}

@Test
fun parse_purchase_with_should_dismiss_true_returns_Purchase_with_shouldDismiss_true() {
Given("a JSON with purchase event with should_dismiss true") {
val json =
"""
{
"version": 1,
"payload": {
"events": [
{
"event_name": "purchase",
"product": "primary",
"product_identifier": "com.app.subscription.monthly",
"should_dismiss": true
}
]
}
}
""".trimIndent()

When("parsed") {
val result = parseWrappedPaywallMessages(json)

Then("it returns Purchase with shouldDismiss true") {
assertTrue(result.isSuccess)
val messages = result.getOrThrow().payload.messages
assertEquals(1, messages.size)
val message = messages[0] as PaywallMessage.Purchase
assertEquals("primary", message.product)
assertEquals("com.app.subscription.monthly", message.productId)
assertEquals(true, message.shouldDismiss)
}
}
}
}

@Test
fun parse_purchase_with_should_dismiss_false_returns_Purchase_with_shouldDismiss_false() {
Given("a JSON with purchase event with explicit should_dismiss false") {
val json =
"""
{
"version": 1,
"payload": {
"events": [
{
"event_name": "purchase",
"product": "secondary",
"product_identifier": "com.app.subscription.yearly",
"should_dismiss": false
}
]
}
}
""".trimIndent()

When("parsed") {
val result = parseWrappedPaywallMessages(json)

Then("it returns Purchase with shouldDismiss false") {
assertTrue(result.isSuccess)
val messages = result.getOrThrow().payload.messages
assertEquals(1, messages.size)
val message = messages[0] as PaywallMessage.Purchase
assertEquals("secondary", message.product)
assertEquals("com.app.subscription.yearly", message.productId)
assertEquals(false, message.shouldDismiss)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import com.superwall.sdk.storage.EventsQueue
import com.superwall.sdk.storage.Storage
import com.superwall.sdk.store.InternalPurchaseController
import com.superwall.sdk.store.StoreManager
import com.superwall.sdk.store.abstractions.product.BasePlanType
import com.superwall.sdk.store.abstractions.product.OfferType
import com.superwall.sdk.store.abstractions.product.RawStoreProduct
import com.superwall.sdk.store.abstractions.product.StoreProduct
Expand Down Expand Up @@ -77,7 +78,7 @@ class TransactionManagerTest {
RawStoreProduct(
playProduct,
"product1",
"basePlan",
BasePlanType.from("basePlan"),
OfferType.Auto,
)

Expand Down
4 changes: 4 additions & 0 deletions superwall/src/main/java/com/superwall/sdk/Superwall.kt
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,7 @@ class Superwall(
TransactionManager.PurchaseSource.ExternalPurchase(
StoreProduct(RawStoreProduct.from(product)),
),
shouldDismiss = true,
)
}.toResult()

Expand All @@ -1012,6 +1013,7 @@ class Superwall(
TransactionManager.PurchaseSource.ExternalPurchase(
product,
),
shouldDismiss = true,
)
}.toResult()

Expand All @@ -1035,6 +1037,7 @@ class Superwall(
TransactionManager.PurchaseSource.ExternalPurchase(
it,
),
shouldDismiss = true,
)
} ?: throw IllegalArgumentException("Product with id $productId not found")
}.toResult()
Expand Down Expand Up @@ -1281,6 +1284,7 @@ class Superwall(
paywallEvent.productId,
paywallView.controller.state,
),
shouldDismiss = paywallEvent.shouldDismiss,
)
} finally {
// Ensure the task is cleared once the purchase is complete or if an error occurs
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ class DependencyContainer(
return@TransactionManager
}

paywallView.webView.messageHandler.handle(PaywallMessage.TransactionComplete(id))
// Schedule fallback notifications from the paywall config in case the paywall
// hasn't been updated to send the ScheduleNotification message dynamically.
// If the paywall sends a ScheduleNotification message, it will cancel and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.superwall.sdk.permissions.PermissionType
import com.superwall.sdk.storage.core_data.convertFromJsonElement
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.booleanOrNull
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
Expand Down Expand Up @@ -64,6 +65,7 @@ sealed class PaywallMessage {
data class Purchase(
val product: String,
val productId: String,
val shouldDismiss: Boolean,
) : PaywallMessage()

data class Custom(
Expand All @@ -81,6 +83,10 @@ sealed class PaywallMessage {

object TransactionStart : PaywallMessage()

data class TransactionComplete(
val productIdentifier: String,
) : PaywallMessage()

data class TrialStarted(
val trialEndDate: Long?,
val productIdentifier: String,
Expand Down Expand Up @@ -163,6 +169,7 @@ private fun parsePaywallMessage(json: JsonObject): PaywallMessage {
PaywallMessage.Purchase(
json["product"]!!.jsonPrimitive.content,
json["product_identifier"]!!.jsonPrimitive.content,
json["should_dismiss"]?.jsonPrimitive?.booleanOrNull ?: true,
)

"custom" -> PaywallMessage.Custom(json["data"]!!.jsonPrimitive.content)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,12 @@ class PaywallMessageHandler(

is PaywallMessage.OpenDeepLink -> openDeepLink(message.url.toString())
is PaywallMessage.Restore -> restorePurchases()
is PaywallMessage.Purchase -> purchaseProduct(withId = message.productId)
is PaywallMessage.Purchase ->
purchaseProduct(
withId = message.productId,
shouldDismiss = message.shouldDismiss,
)

is PaywallMessage.PaywallOpen -> {
if (messageHandler?.state?.paywall?.paywalljsVersion == null) {
queue.offer(message)
Expand Down Expand Up @@ -203,6 +208,16 @@ class PaywallMessageHandler(
setAttributes(message.data)
}

is PaywallMessage.TransactionComplete -> {
ioScope.launch {
pass(
SuperwallEvents.TransactionComplete.rawName,
paywall,
mapOf("product_identifier" to message.productIdentifier),
)
}
}

is PaywallMessage.TrialStarted -> {
ioScope.launch {
pass(
Expand Down Expand Up @@ -460,10 +475,13 @@ class PaywallMessageHandler(
messageHandler?.eventDidOccur(PaywallWebEvent.InitiateRestore)
}

private fun purchaseProduct(withId: String) {
private fun purchaseProduct(
withId: String,
shouldDismiss: Boolean,
) {
detectHiddenPaywallEvent("purchase")
hapticFeedback()
messageHandler?.eventDidOccur(PaywallWebEvent.InitiatePurchase(withId))
messageHandler?.eventDidOccur(PaywallWebEvent.InitiatePurchase(withId, shouldDismiss))
}

private fun handleCustomEvent(customEvent: String) {
Expand Down Expand Up @@ -559,6 +577,7 @@ class PaywallMessageHandler(
),
)
}

PermissionStatus.DENIED, PermissionStatus.UNSUPPORTED -> {
track(
InternalSuperwallEvent.Permission(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ sealed class PaywallWebEvent {
@SerialName("initiate_purchase")
data class InitiatePurchase(
val productId: String,
val shouldDismiss: Boolean,
) : PaywallWebEvent()

object InitiateRestore : PaywallWebEvent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ class TransactionManager(
PurchaseSource.ObserverMode(product),
product.hasFreeTrial,
purchase,
shouldDismiss = true, // gets ignored later on,
)
}
}
Expand Down Expand Up @@ -270,7 +271,10 @@ class TransactionManager(
storage.write(PurchasingProductdIds, remainingTransactions.toSet())
}

suspend fun purchase(purchaseSource: PurchaseSource): PurchaseResult {
suspend fun purchase(
purchaseSource: PurchaseSource,
shouldDismiss: Boolean = true,
): PurchaseResult {
val product =
when (purchaseSource) {
is PurchaseSource.Internal ->
Expand Down Expand Up @@ -318,7 +322,7 @@ class TransactionManager(

when (result) {
is PurchaseResult.Purchased -> {
didPurchase(product, purchaseSource, isEligibleForTrial && product.hasFreeTrial)
didPurchase(product, purchaseSource, isEligibleForTrial && product.hasFreeTrial, shouldDismiss = shouldDismiss)
}

is PurchaseResult.Failed -> {
Expand Down Expand Up @@ -568,6 +572,7 @@ class TransactionManager(
purchaseSource: PurchaseSource,
didStartFreeTrial: Boolean,
purchase: Purchase? = null,
shouldDismiss: Boolean,
) {
when (purchaseSource) {
is PurchaseSource.Internal -> {
Expand All @@ -592,7 +597,7 @@ class TransactionManager(

trackTransactionDidSucceed(transaction, product, purchaseSource, didStartFreeTrial)

if (factory.makeSuperwallOptions().paywalls.automaticallyDismiss) {
if (shouldDismiss && factory.makeSuperwallOptions().paywalls.automaticallyDismiss) {
dismiss(
purchaseSource.paywallInfo.cacheKey,
PaywallResult.Purchased(product.fullIdentifier),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class PaywallMessageHandlerTest {

When("purchase message is handled") {
val productId = "com.example.product"
harness.handler.handle(PaywallMessage.Purchase("Monthly Plan", productId))
harness.handler.handle(PaywallMessage.Purchase("Monthly Plan", productId, shouldDismiss = true))
latch.await(500, TimeUnit.MILLISECONDS)

Then("InitiatePurchase event is delivered with product id") {
Expand Down
Loading
Loading