Skip to content

Commit 593b566

Browse files
IAP Prototype.
1 parent 6f12d7c commit 593b566

21 files changed

+637
-0
lines changed

iap-example/build.gradle

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
apply from: configs.androidApplication
2+
3+
apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
4+
apply plugin: 'org.jetbrains.kotlin.plugin.compose'
5+
6+
android {
7+
defaultConfig {
8+
applicationId "com.stripe.android.iap.example"
9+
versionCode 20
10+
11+
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
12+
}
13+
14+
buildFeatures {
15+
compose true
16+
}
17+
}
18+
19+
dependencies {
20+
implementation project(':iap')
21+
22+
implementation libs.accompanist.materialThemeAdapter
23+
implementation libs.androidx.appCompat
24+
implementation libs.androidx.coreKtx
25+
implementation libs.androidx.lifecycleCompose
26+
implementation libs.compose.activity
27+
implementation libs.compose.material
28+
implementation libs.compose.materialIcons
29+
implementation libs.compose.ui
30+
implementation libs.compose.viewModels
31+
implementation libs.kotlin.serialization
32+
implementation libs.material
33+
}

iap-example/gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
STRIPE_ANDROID_NAMESPACE=com.stripe.android.iapexample

iap-example/proguard-rules.pro

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.INTERNET" />
5+
6+
<application
7+
android:allowBackup="true"
8+
android:fullBackupOnly="true"
9+
android:label="IAP Example"
10+
android:theme="@style/Theme.Material3.DayNight"
11+
android:supportsRtl="true">
12+
<activity
13+
android:name=".MainActivity"
14+
android:exported="true">
15+
<intent-filter>
16+
<action android:name="android.intent.action.MAIN" />
17+
<category android:name="android.intent.category.LAUNCHER" />
18+
</intent-filter>
19+
</activity>
20+
</application>
21+
22+
</manifest>
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.stripe.android.iapexample
2+
3+
import android.os.Bundle
4+
import android.widget.Toast
5+
import androidx.activity.compose.setContent
6+
import androidx.activity.viewModels
7+
import androidx.appcompat.app.AppCompatActivity
8+
import androidx.compose.foundation.layout.Column
9+
import androidx.compose.material.Button
10+
import androidx.compose.material.Text
11+
import com.stripe.android.iap.InAppPurchase
12+
import com.stripe.android.iap.InAppPurchasePluginLookupType
13+
14+
internal class MainActivity : AppCompatActivity(), InAppPurchase.ResultCallback {
15+
16+
private val viewModel by viewModels<MainActivityViewModel>()
17+
18+
override fun onCreate(savedInstanceState: Bundle?) {
19+
super.onCreate(savedInstanceState)
20+
21+
val inAppPurchase = InAppPurchase(
22+
publishableKey = "pk_test_123",
23+
activity = this,
24+
resultCallback = this,
25+
plugins = viewModel.plugins,
26+
)
27+
28+
setContent {
29+
Column {
30+
Button(
31+
onClick = { inAppPurchase.purchase("price_123") }
32+
) {
33+
Text("Default")
34+
}
35+
36+
Button(
37+
onClick = { }
38+
) {
39+
Text("Checkout")
40+
}
41+
42+
Button(
43+
onClick = {
44+
inAppPurchase.purchase("price_123", lookupType = InAppPurchasePluginLookupType.GooglePlay())
45+
}
46+
) {
47+
Text("Google Play")
48+
}
49+
}
50+
}
51+
}
52+
53+
override fun onResult(result: InAppPurchase.Result) {
54+
when (result) {
55+
is InAppPurchase.Result.Canceled -> {
56+
Toast.makeText(this, "Canceled", Toast.LENGTH_LONG).show()
57+
}
58+
is InAppPurchase.Result.Completed -> {
59+
// Just call your server, given the authenticated customer, and look up subscription
60+
Toast.makeText(this, "Completed", Toast.LENGTH_LONG).show()
61+
}
62+
is InAppPurchase.Result.Failed -> {
63+
Toast.makeText(this, "Failed", Toast.LENGTH_LONG).show()
64+
}
65+
}
66+
}
67+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.stripe.android.iapexample
2+
3+
import androidx.lifecycle.ViewModel
4+
import com.stripe.android.iap.InAppPurchasePlugin
5+
6+
internal class MainActivityViewModel : ViewModel() {
7+
val plugins = listOf(
8+
InAppPurchasePlugin.stripeCheckout { "https://checkout.stripe.com/c/pay/cs_test_a1HBtfleCxMwy8n2EVIVG0GWBan1iL5UneGLlSQZfXk9WntapuOf22Zu7e#fid2cGd2ZndsdXFsamtQa2x0cGBrYHZ2QGtkZ2lgYSc%2FY2RpdmApJ2R1bE5gfCc%2FJ3VuWnFgdnFaMDRNc1FMMklwMGo2VTQ9X3UzcTBEYkdWbkhzUmpRcUQ1a3xEMnVTXEF0dWNJbldxUnBrMnRfUVxGSk1GV2B1d2NJSDEzMXxkR2BDMjdQQ2NHMmZcPFJCMWQ1NV9rQXFsRjdGJyknY3dqaFZgd3Ngdyc%2FcXdwYCknaWR8anBxUXx1YCc%2FJ3Zsa2JpYFpscWBoJyknYGtkZ2lgVWlkZmBtamlhYHd2Jz9xd3BgeCUl" },
9+
InAppPurchasePlugin.googlePlay { "cscs_1234" },
10+
)
11+
}

iap/build.gradle

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
apply from: configs.androidLibrary
2+
3+
apply plugin: 'org.jetbrains.kotlin.plugin.parcelize'
4+
apply plugin: 'org.jetbrains.kotlin.plugin.serialization'
5+
apply plugin: 'com.google.devtools.ksp'
6+
apply plugin: 'dev.drewhamilton.poko'
7+
8+
dependencies {
9+
implementation libs.androidx.browser
10+
api libs.kotlin.coroutines
11+
implementation libs.kotlin.serialization
12+
api libs.androidx.activity
13+
implementation libs.androidx.lifecycle
14+
implementation libs.androidx.appCompat
15+
16+
implementation "com.android.billingclient:billing-ktx:7.1.1"
17+
18+
ksp libs.daggerCompiler
19+
20+
testImplementation testLibs.junit
21+
testImplementation testLibs.robolectric
22+
testImplementation testLibs.truth
23+
}
24+
25+
android {
26+
testOptions {
27+
unitTests {
28+
includeAndroidResources = true
29+
all {
30+
maxHeapSize = "1024m"
31+
}
32+
}
33+
// Make sure animations are off when we run espresso tests
34+
animationsDisabled = true
35+
}
36+
}
37+
38+
ext {
39+
artifactId = "iap"
40+
artifactName = "iap"
41+
artifactDescrption = "Stripe In App Purchase Android SDK"
42+
}
43+
44+
//apply from: "${rootDir}/deploy/deploy.gradle"

iap/consumer-rules.txt

Whitespace-only changes.

iap/gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
STRIPE_ANDROID_NAMESPACE=com.stripe.android.iap

iap/res/values/themes.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<!-- Theme used by Activities that do not show any UI, so the activity underneath them is still fully visible -->
4+
<style name="StripeIapTransparentTheme" parent="@style/Theme.AppCompat.NoActionBar">
5+
<item name="android:windowSoftInputMode">adjustResize</item>
6+
<item name="android:windowIsTranslucent">true</item>
7+
<item name="android:windowBackground">@android:color/transparent</item>
8+
<item name="android:windowContentOverlay">@null</item>
9+
<item name="android:windowNoTitle">true</item>
10+
<item name="android:windowIsFloating">true</item>
11+
<item name="android:backgroundDimEnabled">false</item>
12+
</style>
13+
</resources>

iap/src/main/AndroidManifest.xml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<application>
5+
<activity
6+
android:name=".CheckoutForegroundActivity"
7+
android:autoRemoveFromRecents="true"
8+
android:configChanges="orientation|keyboard|keyboardHidden|screenLayout|screenSize|smallestScreenSize"
9+
android:launchMode="singleTop"
10+
android:theme="@style/StripeIapTransparentTheme" />
11+
12+
<activity
13+
android:name=".CheckoutRedirectHandlerActivity"
14+
android:theme="@style/StripeIapTransparentTheme"
15+
android:autoRemoveFromRecents="true"
16+
android:launchMode="singleInstance"
17+
android:exported="true">
18+
<intent-filter>
19+
<action android:name="android.intent.action.VIEW" />
20+
21+
<category android:name="android.intent.category.DEFAULT" />
22+
<category android:name="android.intent.category.BROWSABLE" />
23+
24+
<data
25+
android:scheme="stripesdk"
26+
android:host="iap_success_url"
27+
android:path="/${applicationId}" />
28+
</intent-filter>
29+
</activity>
30+
</application>
31+
</manifest>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.stripe.android.iap
2+
3+
import android.content.ActivityNotFoundException
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.net.Uri
7+
import android.os.Bundle
8+
import androidx.appcompat.app.AppCompatActivity
9+
import androidx.browser.customtabs.CustomTabsIntent
10+
import androidx.core.net.toUri
11+
12+
internal class CheckoutForegroundActivity : AppCompatActivity() {
13+
companion object {
14+
const val ACTION_REDIRECT = "CheckoutForegroundActivity.redirect"
15+
const val EXTRA_CHECKOUT_URL = "CheckoutUrl"
16+
const val EXTRA_FAILURE = "CheckoutFailure"
17+
const val RESULT_COMPLETE = 49871
18+
const val RESULT_FAILURE = 91367
19+
20+
private const val SAVED_STATE_HAS_LAUNCHED_URL = "CheckoutForegroundActivity_LaunchedUrl"
21+
22+
fun createIntent(context: Context, checkoutUrl: String): Intent {
23+
return Intent(context, CheckoutForegroundActivity::class.java)
24+
.putExtra(EXTRA_CHECKOUT_URL, checkoutUrl)
25+
}
26+
27+
fun redirectIntent(context: Context, uri: Uri?): Intent {
28+
val intent = Intent(context, CheckoutForegroundActivity::class.java)
29+
intent.action = ACTION_REDIRECT
30+
intent.data = uri
31+
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
32+
return intent
33+
}
34+
}
35+
36+
private var hasLaunchedUrl: Boolean = false
37+
38+
override fun onCreate(savedInstanceState: Bundle?) {
39+
super.onCreate(savedInstanceState)
40+
41+
hasLaunchedUrl = savedInstanceState?.getBoolean(SAVED_STATE_HAS_LAUNCHED_URL) ?: false
42+
43+
handleRedirectIfAvailable(intent)
44+
}
45+
46+
override fun onSaveInstanceState(outState: Bundle) {
47+
super.onSaveInstanceState(outState)
48+
outState.putBoolean(SAVED_STATE_HAS_LAUNCHED_URL, hasLaunchedUrl)
49+
}
50+
51+
override fun onNewIntent(intent: Intent) {
52+
super.onNewIntent(intent)
53+
handleRedirectIfAvailable(intent)
54+
}
55+
56+
override fun onResume() {
57+
super.onResume()
58+
59+
if (!isFinishing) {
60+
if (hasLaunchedUrl) {
61+
setResult(RESULT_CANCELED)
62+
finish()
63+
} else {
64+
launchCheckoutUrl()
65+
}
66+
}
67+
}
68+
69+
private fun handleRedirectIfAvailable(intent: Intent) {
70+
if (intent.action == ACTION_REDIRECT) {
71+
setResult(RESULT_COMPLETE, intent)
72+
finish()
73+
}
74+
}
75+
76+
private fun launchCheckoutUrl() {
77+
hasLaunchedUrl = true
78+
79+
val checkoutUri = intent.extras?.getString(EXTRA_CHECKOUT_URL)?.toUri()
80+
if (checkoutUri == null) {
81+
setResult(RESULT_CANCELED)
82+
finish()
83+
return
84+
}
85+
try {
86+
CustomTabsIntent.Builder()
87+
.setShareState(CustomTabsIntent.SHARE_STATE_OFF)
88+
.build()
89+
.launchUrl(this, checkoutUri)
90+
} catch (e: ActivityNotFoundException) {
91+
setResult(RESULT_FAILURE, Intent().putExtra(EXTRA_FAILURE, e))
92+
finish()
93+
}
94+
}
95+
}

0 commit comments

Comments
 (0)