Skip to content

Commit

Permalink
Merge pull request #72 from superwall-me/develop
Browse files Browse the repository at this point in the history
v1.0.0-alpha.27
  • Loading branch information
yusuftor authored Dec 6, 2023
2 parents cce3b6f + 7f24dcb commit 48fd487
Show file tree
Hide file tree
Showing 62 changed files with 2,370 additions and 877 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ name: Build, Test & Publish
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
contents: write
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/build+test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
# This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle

name: Build, Test

on:
pull_request:
branches: [ "main" ]

# Prevents running a bunch of we push right back to back
concurrency:
group: test-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set Up JDK
uses: actions/setup-java@v3
with:
distribution: 'zulu' # See 'Supported distributions' for available options
java-version: '17'
cache: 'gradle'

# Allow us to run the command
- name: Change wrapper permissions
run: chmod +x ./gradlew

# Run Build & Test the Project
- name: Build gradle project
run: ./gradlew build
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@

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

## 1.0.0-alpha.27

### Breaking Changes

- #SW-2218: Changes the `PurchaseController` purchase function to `purchase(activity:productDetails:basePlanId:offerId:)`.
This adds support for purchasing offers and base plans.

### Enhancements

- #SW-2600: Backport device attributes
- Adds support for localized paywalls.
- Paywalls are only preloaded if their associated rules can match.
- Adds status bar to full screen paywalls.

### Fixes

- Fixes issue where holdouts were still matching even if the limit set for their corresponding rules were exceeded.
- #SW-2584: Fixes issue where prices with non-terminating decimals were causing products to fail to load.

## 1.0.0-alpha.26

### Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class MainApplication : android.app.Application(), SuperwallDelegate {
SessionStart: pk_6c881299e2f8db59f697646e399397be76432fa0968ca254
PaywallDecline: pk_a1071d541642719e2dc854da9ec717ec967b8908854ede74
TransactionAbandon: pk_9c99186b023ae795e0189cf9cdcd3e2d2d174289e0800d66
TransacionFail: pk_b6cd945401435766da627080a3fbe349adb2dcd69ab767f3
TransactionFail: pk_b6cd945401435766da627080a3fbe349adb2dcd69ab767f3
SurveyResponse: pk_3698d9fe123f1e4aa8014ceca111096ca06fd68d31d9e662
*/
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ package com.superwall.superapp

import android.app.Activity
import android.content.Context
import com.android.billingclient.api.SkuDetails
import com.android.billingclient.api.ProductDetails
import com.revenuecat.purchases.*
import com.revenuecat.purchases.interfaces.GetStoreProductsCallback
import com.revenuecat.purchases.interfaces.PurchaseCallback
import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
import com.revenuecat.purchases.models.StoreProduct
import com.revenuecat.purchases.models.StoreTransaction
import com.revenuecat.purchases.models.SubscriptionOption
import com.revenuecat.purchases.models.googleProduct
import com.superwall.sdk.Superwall
import com.superwall.sdk.delegate.PurchaseResult
import com.superwall.sdk.delegate.RestorationResult
Expand Down Expand Up @@ -73,7 +75,6 @@ suspend fun Purchases.awaitRestoration(): CustomerInfo {
}

class RevenueCatPurchaseController(val context: Context): PurchaseController, UpdatedCustomerInfoListener {

init {
Purchases.logLevel = LogLevel.DEBUG
Purchases.configure(PurchasesConfiguration.Builder(context, "goog_DCSOujJzRNnPmxdgjOwdOOjwilC").build())
Expand Down Expand Up @@ -107,23 +108,80 @@ class RevenueCatPurchaseController(val context: Context): PurchaseController, Up
/**
* Initiate a purchase
*/
override suspend fun purchase(activity: Activity, product: SkuDetails): PurchaseResult {
val products = Purchases.sharedInstance.awaitProducts(listOf(product.sku))
val product = products.firstOrNull()
?: return PurchaseResult.Failed(Exception("Product not found"))
return try {
Purchases.sharedInstance.awaitPurchase(activity, product)
override suspend fun purchase(
activity: Activity,
productDetails: ProductDetails,
basePlanId: String?,
offerId: String?
): PurchaseResult {
// Find products matching productId from RevenueCat
val products = Purchases.sharedInstance.awaitProducts(listOf(productDetails.productId))

// Choose the product which matches the given base plan.
// If no base plan set, select first product or fail.
val product = products.firstOrNull { it.googleProduct?.basePlanId == basePlanId }
?: products.firstOrNull()
?: return PurchaseResult.Failed("Product not found")

return when (product.type) {
ProductType.SUBS, ProductType.UNKNOWN -> handleSubscription(activity, product, basePlanId, offerId)
ProductType.INAPP -> handleInAppPurchase(activity, product)
}
}

private fun buildSubscriptionOptionId(basePlanId: String?, offerId: String?): String =
buildString {
basePlanId?.let { append("$it") }
offerId?.let { append(":$it") }
}

private suspend fun handleSubscription(
activity: Activity,
storeProduct: StoreProduct,
basePlanId: String?,
offerId: String?
): PurchaseResult {
storeProduct.subscriptionOptions?.let { subscriptionOptions ->
// If subscription option exists, concatenate base + offer ID.
val subscriptionOptionId = buildSubscriptionOptionId(basePlanId, offerId)

// Find first subscription option that matches the subscription option ID or default
// to letting revenuecat choose.
val subscriptionOption = subscriptionOptions.firstOrNull { it.id == subscriptionOptionId }
?: subscriptionOptions.defaultOffer

// Purchase subscription option, otherwise fail.
if (subscriptionOption != null) {
return purchaseSubscription(activity, subscriptionOption)
}
}
return PurchaseResult.Failed("Valid subscription option not found for product.")
}

private suspend fun purchaseSubscription(activity: Activity, subscriptionOption: SubscriptionOption): PurchaseResult {
val deferred = CompletableDeferred<PurchaseResult>()
Purchases.sharedInstance.purchaseWith(
PurchaseParams.Builder(activity, subscriptionOption).build(),
onError = { error, userCancelled ->
deferred.complete(if (userCancelled) PurchaseResult.Cancelled() else PurchaseResult.Failed(error.message))
},
onSuccess = { _, _ ->
deferred.complete(PurchaseResult.Purchased())
}
)
return deferred.await()
}

private suspend fun handleInAppPurchase(activity: Activity, storeProduct: StoreProduct): PurchaseResult =
try {
Purchases.sharedInstance.awaitPurchase(activity, storeProduct)
PurchaseResult.Purchased()
} catch (e: PurchasesException) {
if (e.purchasesError.code === PurchasesErrorCode.PurchaseCancelledError) {
// Purchase was cancelled by the user
PurchaseResult.Cancelled()
} else {
// Some other error occurred
PurchaseResult.Failed(e)
when (e.purchasesError.code) {
PurchasesErrorCode.PurchaseCancelledError -> PurchaseResult.Cancelled()
else -> PurchaseResult.Failed(e.message ?: "Purchase failed due to an unknown error")
}
}
}

/**
* Restore purchases
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ fun UITestTable() {
UITestHandler.test74Info to { CoroutineScope(Dispatchers.IO).launch { UITestHandler.test74() } },
UITestHandler.test75Info to { CoroutineScope(Dispatchers.IO).launch { UITestHandler.test75() } },
UITestHandler.test82Info to { CoroutineScope(Dispatchers.IO).launch { UITestHandler.test82() } },
UITestHandler.test83Info to { CoroutineScope(Dispatchers.IO).launch { UITestHandler.test83() } },
UITestHandler.testAndroid4Info to { CoroutineScope(Dispatchers.IO).launch { UITestHandler.testAndroid4() } },
UITestHandler.testAndroid9Info to { CoroutineScope(Dispatchers.IO).launch { UITestHandler.testAndroid9() } },
UITestHandler.testAndroid18Info to { CoroutineScope(Dispatchers.IO).launch { UITestHandler.testAndroid18() } },
Expand Down
12 changes: 11 additions & 1 deletion app/src/main/java/com/superwall/superapp/test/UITestHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1303,7 +1303,7 @@ class UITestHandler {
when (eventInfo.event) {
is SuperwallEvent.TransactionComplete -> {
val transaction = (eventInfo.event as SuperwallEvent.TransactionComplete).transaction == null
val productId = (eventInfo.event as SuperwallEvent.TransactionComplete).product.productIdentifier
val productId = (eventInfo.event as SuperwallEvent.TransactionComplete).product.fullIdentifier
val paywallId = (eventInfo.event as SuperwallEvent.TransactionComplete).paywallInfo.identifier

println("!!! TEST 75 !!! TransactionComplete. Transaction nil? $transaction, $productId, $paywallId")
Expand All @@ -1325,6 +1325,16 @@ class UITestHandler {
Superwall.instance.register(event = "price_readout")
}

var test83Info = UITestInfo(
83,
"The first time launch is tapped you will land in holdout. Check Skipped paywall " +
"for holdout printed in console. Tapping launch again will present paywall. Will " +
"need to delete app to be able to do it again as it uses limits."
)
suspend fun test83() {
Superwall.instance.register(event = "holdout_one_time_occurrence")
}

var testAndroid4Info = UITestInfo(
4,
"NOTE: Must use `Android Main screen` API key. Launch compose debug screen: " +
Expand Down
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
[versions]
billing_version = "5.2.1"
billing_version = "6.1.0"
browser_version = "1.5.0"
gradle_plugin_version = "7.4.2"
mockk = "1.13.8"
revenue_cat_version = "6.0.0"
compose_version = "2022.10.00"
kotlinx_serialization_json_version = "1.5.1"
Expand All @@ -21,6 +22,7 @@ test_runner_version = "1.4.0"
test_rules_version = "1.4.0"
kotlin = "1.8.21"
kotlinx_coroutines_core_version = "1.7.1"
mockk_version = "1.12.8"


[libraries]
Expand Down Expand Up @@ -58,6 +60,7 @@ gson = { module = "com.google.code.gson:gson", version.ref = "gson_version" }
# Test
junit = { module = "junit:junit", version.ref = "junit_version" }
kotlinx_coroutines_test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx_coroutines_test_version" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk_version" }

# Test (Android)
test_ext_junit = { module = "androidx.test.ext:junit", version.ref = "test_ext_junit_version" }
Expand Down
7 changes: 5 additions & 2 deletions superwall/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ plugins {
id("maven-publish")
}

version = "1.0.0-alpha.26"
version = "1.0.0-alpha.27"

android {
compileSdk = 33
Expand Down Expand Up @@ -73,7 +73,9 @@ android {
jvmTarget = "1.8"
}

testOptions { }
packagingOptions {
resources.excludes += "META-INF/LICENSE.md"
}

publishing {
singleVariant("release") {
Expand Down Expand Up @@ -161,6 +163,7 @@ dependencies {
// Test
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.mockk)
// ??? Not sure if we need this
// testImplementation("org.json:json:20210307")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.superwall.sdk.config

import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import com.superwall.sdk.config.models.ConfigState
import com.superwall.sdk.dependencies.DependencyContainer
Expand All @@ -25,11 +26,13 @@ import org.junit.Test


class ConfigManagerUnderTest(
private val context: Context,
private val storage: Storage,
private val network: Network,
private val paywallManager: PaywallManager,
private val factory: Factory,
) : ConfigManager(
context = context,
storage = storage,
network = network,
paywallManager = paywallManager,
Expand All @@ -56,10 +59,11 @@ class ConfigManagerTests {
paywallId = "jkl"
)
val assignment = ConfirmableAssignment(experimentId = experimentId, variant = variant)
val dependencyContainer = DependencyContainer(context, null, null)
val dependencyContainer = DependencyContainer(context, null, null, activityProvider = null)
val network = NetworkMock(factory = dependencyContainer)
val storage = StorageMock(context = context)
val configManager = ConfigManager(
context = context,
options = null,
// storeKitManager = dependencyContainer.storeKitManager,
storage = storage,
Expand All @@ -82,12 +86,12 @@ class ConfigManagerTests {
// get context
val context = InstrumentationRegistry.getInstrumentation().targetContext

val dependencyContainer = DependencyContainer(context, null, null)
val dependencyContainer = DependencyContainer(context, null, null, activityProvider = null)
val network = NetworkMock(factory = dependencyContainer)
val storage = StorageMock(context = context)
val configManager = ConfigManager(
context = context,
options = null,
// storeKitManager = dependencyContainer.storeKitManager,
storage = storage,
network = network,
paywallManager = dependencyContainer.paywallManager,
Expand Down Expand Up @@ -115,11 +119,11 @@ class ConfigManagerTests {
// get context
val context = InstrumentationRegistry.getInstrumentation().targetContext

val dependencyContainer = DependencyContainer(context, null, null)
val dependencyContainer = DependencyContainer(context, null, null, activityProvider = null)
val network = NetworkMock(factory = dependencyContainer)
val storage = StorageMock(context = context)
val configManager = ConfigManagerUnderTest(
// storeKitManager = dependencyContainer.storeKitManager,
context = context,
storage = storage,
network = network,
paywallManager = dependencyContainer.paywallManager,
Expand All @@ -141,11 +145,11 @@ class ConfigManagerTests {
// get context
val context = InstrumentationRegistry.getInstrumentation().targetContext

val dependencyContainer = DependencyContainer(context, null, null)
val dependencyContainer = DependencyContainer(context, null, null, activityProvider = null)
val network = NetworkMock(factory = dependencyContainer)
val storage = StorageMock(context = context)
val configManager = ConfigManagerUnderTest(
// storeKitManager = dependencyContainer.storeKitManager,
context = context,
storage = storage,
network = network,
paywallManager = dependencyContainer.paywallManager,
Expand Down
Loading

0 comments on commit 48fd487

Please sign in to comment.