.
675 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Google-IAP (Play Billing Library Version 7.0.0)
2 |
3 |
4 |
6 |
7 |
8 |
9 |
10 |
11 |
13 |
14 |
16 |
17 |
19 |
20 |
21 |
22 | Features •
23 | Development •
24 | Usage •
25 | License •
26 | Contribution
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 | ---
35 |
36 | [](https://postimg.cc/Q9hGcs1f)
37 |
38 | IAP is an Android library to handle In-App purchases with minimal code.
39 |
40 | ## Features
41 |
42 | * Written in Kotlin
43 | * No boilerplate code
44 | * Easy initialization
45 | * Supports InApp & Subscription products
46 | * Simple configuration for consumable products
47 |
48 | ## Gradle Dependency
49 |
50 | * Add the JitPack repository to your project's build.gradle file
51 |
52 | ```groovy
53 | allprojects {
54 | repositories {
55 | ...
56 | maven { url 'https://jitpack.io' }
57 | }
58 | }
59 | ```
60 | * Add the dependency in your app's build.gradle file
61 |
62 | ```groovy
63 | dependencies {
64 | implementation 'com.github.akshaaatt:Google-IAP:1.8.0'
65 | }
66 | ```
67 |
68 | ## Development
69 |
70 | * Prerequisite: Latest version of the Android Studio and SDKs on your pc.
71 | * Clone this repository.
72 | * Use the `gradlew build` command to build the project directly or use the IDE to run the project to your phone or the emulator.
73 |
74 | ## Usage
75 |
76 | #### Establishing connection with Play console
77 |
78 | ```kotlin
79 | val iapConnector = IapConnector(
80 | context = this, // activity / context
81 | nonConsumableKeys = nonConsumablesList, // pass the list of non-consumables
82 | consumableKeys = consumablesList, // pass the list of consumables
83 | subscriptionKeys = subsList, // pass the list of subscriptions
84 | key = "LICENSE KEY", // pass your app's license key
85 | enableLogging = true // to enable / disable logging
86 | )
87 | ```
88 |
89 | #### Receiving events
90 |
91 | ```kotlin
92 | iapConnector.addPurchaseListener(object : PurchaseServiceListener {
93 | override fun onPricesUpdated(iapKeyPrices: Map) {
94 | // list of available products will be received here, so you can update UI with prices if needed
95 | }
96 |
97 | override fun onProductPurchased(purchaseInfo: DataWrappers.PurchaseInfo) {
98 | // will be triggered whenever purchase succeeded
99 | }
100 |
101 | override fun onProductRestored(purchaseInfo: DataWrappers.PurchaseInfo) {
102 | // will be triggered fetching owned products using IapConnector
103 | }
104 | })
105 |
106 | iapConnector.addSubscriptionListener(object : SubscriptionServiceListener {
107 | override fun onSubscriptionRestored(purchaseInfo: DataWrappers.PurchaseInfo) {
108 | // will be triggered upon fetching owned subscription upon initialization
109 | }
110 |
111 | override fun onSubscriptionPurchased(purchaseInfo: DataWrappers.PurchaseInfo) {
112 | // will be triggered whenever subscription succeeded
113 | }
114 |
115 | override fun onPricesUpdated(iapKeyPrices: Map) {
116 | // list of available products will be received here, so you can update UI with prices if needed
117 | }
118 | })
119 | ```
120 |
121 | #### Making a purchase
122 |
123 | ```kotlin
124 | iapConnector.purchase(this, "")
125 | ```
126 |
127 | #### Making a subscription
128 |
129 | ```kotlin
130 | iapConnector.subscribe(this, "")
131 | ```
132 |
133 | #### Removing a subscription
134 |
135 | ```kotlin
136 | iapConnector.unsubscribe(this, "")
137 | ```
138 |
139 | ## Sample App
140 |
141 | * Add your products to the developer console
142 |
143 | * Replace the key with your App's License Key
144 |
145 |
146 | ## Apps Using this Library
147 |
148 | * https://play.google.com/store/apps/details?id=com.redalck.gameone
149 |
150 | * https://play.google.com/store/apps/details?id=com.redalck.gametwo
151 |
152 | * https://play.google.com/store/apps/details?id=com.redalck.gamethree
153 |
154 | * https://play.google.com/store/apps/details?id=com.redalck.gamefour
155 |
156 | * https://play.google.com/store/apps/details?id=com.redalck.gamefive
157 |
158 | * https://play.google.com/store/apps/details?id=com.redalck.gamesix
159 |
160 | * https://play.google.com/store/apps/details?id=com.redalck.gameseven
161 |
162 | * https://play.google.com/store/apps/details?id=com.redalck.gameeight
163 |
164 | * https://play.google.com/store/apps/details?id=daily.status.earn.money
165 |
166 | ## License
167 |
168 | This Project is licensed under the [GPL version 3 or later](https://www.gnu.org/licenses/gpl-3.0.html).
169 |
170 | ## Contribution
171 |
172 | You are most welcome to contribute to this project!
173 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /google-services.json
3 | /playstore_keyfile.jks
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import java.util.Properties
2 |
3 | plugins {
4 | alias(libs.plugins.android.application)
5 | alias(libs.plugins.kotlin.android)
6 | }
7 |
8 | android {
9 | compileSdk = libs.versions.compileSdk.get().toInt()
10 |
11 | namespace = "com.limurse.iapsample"
12 | defaultConfig {
13 | applicationId = "com.limurse.iapsample"
14 | minSdk = libs.versions.minSdk.get().toInt()
15 | targetSdk = libs.versions.targetSdk.get().toInt()
16 | versionCode = 9
17 | versionName = "1.1.0"
18 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | buildTypes {
22 | val keystoreProperties = Properties().apply {
23 | val keystorePropertiesFile = rootProject.file("keystore.properties")
24 | if (keystorePropertiesFile.exists()) {
25 | load(keystorePropertiesFile.inputStream())
26 | }
27 | }
28 | val licenseKey = keystoreProperties.getProperty("licenseKey")
29 | release {
30 | isMinifyEnabled = true
31 | isShrinkResources = true
32 | proguardFiles("proguard-rules.pro")
33 | resValue("string", "licenseKey", licenseKey)
34 | }
35 | debug {
36 | isMinifyEnabled = false
37 | proguardFiles("proguard-rules.pro")
38 | resValue("string", "licenseKey", licenseKey)
39 | }
40 | }
41 |
42 | buildFeatures {
43 | viewBinding = true
44 | }
45 |
46 | kotlin {
47 | jvmToolchain(17)
48 | }
49 |
50 | compileOptions {
51 | sourceCompatibility = JavaVersion.VERSION_17
52 | targetCompatibility = JavaVersion.VERSION_17
53 | }
54 | }
55 |
56 | dependencies {
57 | implementation(project(":iap"))
58 |
59 | implementation(libs.appcompat)
60 | implementation(libs.gridlayout)
61 | implementation(libs.material)
62 | implementation(libs.constraintlayout)
63 | implementation(libs.sdp.android)
64 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/release/app-release.aab:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/release/app-release.aab
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
24 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/java/com/limurse/iapsample/JavaSampleActivity.java:
--------------------------------------------------------------------------------
1 | package com.limurse.iapsample;
2 |
3 | import android.os.Bundle;
4 | import android.util.Log;
5 | import android.widget.Toast;
6 |
7 | import androidx.annotation.NonNull;
8 | import androidx.appcompat.app.AppCompatActivity;
9 | import androidx.lifecycle.MutableLiveData;
10 |
11 | import com.limurse.iap.DataWrappers;
12 | import com.limurse.iap.IapConnector;
13 | import com.limurse.iap.PurchaseServiceListener;
14 | import com.limurse.iap.SubscriptionServiceListener;
15 | import com.limurse.iapsample.databinding.ActivityMainBinding;
16 | import org.jetbrains.annotations.NotNull;
17 | import org.jetbrains.annotations.Nullable;
18 |
19 | import java.util.Arrays;
20 | import java.util.Collections;
21 | import java.util.List;
22 | import java.util.Map;
23 |
24 | class JavaSampleActivity extends AppCompatActivity {
25 | MutableLiveData isBillingClientConnected = new MutableLiveData<>();
26 |
27 | protected void onCreate(@Nullable Bundle savedInstanceState) {
28 | super.onCreate(savedInstanceState);
29 | ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
30 | setContentView(binding.getRoot());
31 |
32 | isBillingClientConnected.setValue(false);
33 |
34 | List nonConsumablesList = Collections.singletonList("lifetime");
35 | List consumablesList = Arrays.asList("base", "moderate", "quite", "plenty", "yearly");
36 | List subsList = Collections.singletonList("subscription");
37 |
38 | IapConnector iapConnector = new IapConnector(
39 | this,
40 | nonConsumablesList,
41 | consumablesList,
42 | subsList,
43 | getString(R.string.licenseKey),
44 | true
45 | );
46 |
47 |
48 | iapConnector.addBillingClientConnectionListener((status, billingResponseCode) -> {
49 | Log.d("JSA", "This is the status: "+status+" and response code is: "+billingResponseCode);
50 | isBillingClientConnected.setValue(status);
51 | });
52 |
53 | iapConnector.addPurchaseListener(new PurchaseServiceListener() {
54 | public void onPricesUpdated(@NotNull Map iapKeyPrices) {
55 |
56 | }
57 |
58 | public void onProductPurchased(@NonNull DataWrappers.PurchaseInfo purchaseInfo) {
59 | if (purchaseInfo.getSku().equals("plenty")) {
60 |
61 | } else if (purchaseInfo.getSku().equals("yearly")) {
62 |
63 | } else if (purchaseInfo.getSku().equals("moderate")) {
64 |
65 | } else if (purchaseInfo.getSku().equals("base")) {
66 |
67 | } else if (purchaseInfo.getSku().equals("quite")) {
68 |
69 | }
70 | }
71 |
72 | public void onProductRestored(@NonNull DataWrappers.PurchaseInfo purchaseInfo) {
73 |
74 | }
75 |
76 | @Override
77 | public void onPurchaseFailed(@Nullable DataWrappers.PurchaseInfo purchaseInfo, @Nullable Integer billingResponseCode) {
78 | Toast.makeText(getApplicationContext(), "Your purchase has been failed", Toast.LENGTH_SHORT).show();
79 | }
80 | });
81 | iapConnector.addSubscriptionListener(new SubscriptionServiceListener() {
82 | public void onSubscriptionRestored(@NonNull DataWrappers.PurchaseInfo purchaseInfo) {
83 | }
84 |
85 | public void onSubscriptionPurchased(@NonNull DataWrappers.PurchaseInfo purchaseInfo) {
86 | if (purchaseInfo.getSku().equals("subscription")) {
87 |
88 | }
89 | }
90 |
91 | public void onPricesUpdated(@NotNull Map iapKeyPrices) {
92 |
93 | }
94 |
95 | @Override
96 | public void onPurchaseFailed(@Nullable DataWrappers.PurchaseInfo purchaseInfo, @Nullable Integer billingResponseCode) {
97 | }
98 | });
99 |
100 | binding.btPurchaseCons.setOnClickListener(it ->
101 | iapConnector.purchase(this, "base", null, null)
102 | );
103 |
104 | binding.btnMonthly.setOnClickListener(it ->
105 | iapConnector.subscribe(this, "subscription", null, null, null)
106 | );
107 |
108 | binding.btnYearly.setOnClickListener(it ->
109 | iapConnector.subscribe(this, "yearly", null, null, null)
110 | );
111 |
112 | binding.btnQuite.setOnClickListener(it ->
113 | iapConnector.purchase(this, "quite", null, null)
114 | );
115 |
116 | binding.btnModerate.setOnClickListener(it ->
117 | iapConnector.purchase(this, "moderate", null, null)
118 | );
119 |
120 | binding.btnUltimate.setOnClickListener(it ->
121 | iapConnector.purchase(this, "plenty", null, null)
122 | );
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/app/src/main/java/com/limurse/iapsample/KotlinSampleActivity.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iapsample
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import android.widget.Toast
6 | import androidx.appcompat.app.AppCompatActivity
7 | import androidx.lifecycle.MutableLiveData
8 | import com.limurse.iap.BillingClientConnectionListener
9 | import com.limurse.iap.DataWrappers
10 | import com.limurse.iap.IapConnector
11 | import com.limurse.iap.PurchaseServiceListener
12 | import com.limurse.iap.SubscriptionServiceListener
13 | import com.limurse.iapsample.databinding.ActivityMainBinding
14 |
15 | class KotlinSampleActivity : AppCompatActivity() {
16 |
17 | private lateinit var iapConnector: IapConnector
18 | private lateinit var binding: ActivityMainBinding
19 |
20 | val isBillingClientConnected: MutableLiveData = MutableLiveData()
21 |
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | binding = ActivityMainBinding.inflate(layoutInflater)
25 | setContentView(binding.root)
26 |
27 | binding.bottomnavview.itemIconTintList = null
28 | isBillingClientConnected.value = false
29 |
30 | val nonConsumablesList = listOf("lifetime")
31 | val consumablesList = listOf("base", "moderate", "quite")
32 | val subsList = listOf("subscription", "yearly")
33 |
34 | iapConnector = IapConnector(
35 | context = this,
36 | nonConsumableKeys = nonConsumablesList,
37 | consumableKeys = consumablesList,
38 | subscriptionKeys = subsList,
39 | key = getString(R.string.licenseKey),
40 | enableLogging = true
41 | )
42 |
43 | iapConnector.addBillingClientConnectionListener(object : BillingClientConnectionListener {
44 | override fun onConnected(status: Boolean, billingResponseCode: Int) {
45 | Log.d(
46 | "KSA",
47 | "This is the status: $status and response code is: $billingResponseCode"
48 | )
49 | isBillingClientConnected.value = status
50 | }
51 | })
52 |
53 | iapConnector.addPurchaseListener(object : PurchaseServiceListener {
54 | override fun onPricesUpdated(iapKeyPrices: Map) {
55 | // list of available products will be received here, so you can update UI with prices if needed
56 | }
57 |
58 | override fun onProductPurchased(purchaseInfo: DataWrappers.PurchaseInfo) {
59 | when (purchaseInfo.sku) {
60 | "base" -> {
61 | purchaseInfo.orderId
62 | }
63 |
64 | "moderate" -> {
65 |
66 | }
67 |
68 | "quite" -> {
69 |
70 | }
71 |
72 | "plenty" -> {
73 |
74 | }
75 | }
76 | }
77 |
78 | override fun onProductRestored(purchaseInfo: DataWrappers.PurchaseInfo) {
79 | // will be triggered fetching owned products using IapConnector;
80 | }
81 |
82 | override fun onPurchaseFailed(purchaseInfo: DataWrappers.PurchaseInfo?, billingResponseCode: Int?) {
83 | // will be triggered whenever a product purchase is failed
84 | Toast.makeText(applicationContext, "Your purchase has been failed", Toast.LENGTH_SHORT).show()
85 | }
86 | })
87 |
88 | iapConnector.addSubscriptionListener(object : SubscriptionServiceListener {
89 | override fun onSubscriptionRestored(purchaseInfo: DataWrappers.PurchaseInfo) {
90 | // will be triggered upon fetching owned subscription upon initialization
91 | }
92 |
93 | override fun onSubscriptionPurchased(purchaseInfo: DataWrappers.PurchaseInfo) {
94 | // will be triggered whenever subscription succeeded
95 | when (purchaseInfo.sku) {
96 | "subscription" -> {
97 |
98 | }
99 | "yearly" -> {
100 |
101 | }
102 | }
103 | }
104 |
105 | override fun onPricesUpdated(iapKeyPrices: Map) {
106 | // list of available products will be received here, so you can update UI with prices if needed
107 | }
108 |
109 | override fun onPurchaseFailed(purchaseInfo: DataWrappers.PurchaseInfo?, billingResponseCode: Int?) {
110 | // will be triggered whenever subscription purchase is failed
111 | }
112 | })
113 |
114 | isBillingClientConnected.observe(this) {connected->
115 | Log.d("KSA", "limurse $connected")
116 | when(connected) {
117 | true -> {
118 | binding.btPurchaseCons.isEnabled = true
119 | binding.btnMonthly.isEnabled = true
120 | binding.btnYearly.isEnabled = true
121 | binding.btnQuite.isEnabled = true
122 | binding.btnModerate.isEnabled = true
123 | binding.btnUltimate.isEnabled = true
124 |
125 | binding.btPurchaseCons.setOnClickListener {
126 | iapConnector.purchase(this, "base")
127 | }
128 | binding.btnMonthly.setOnClickListener {
129 | iapConnector.subscribe(this, "subscription")
130 | }
131 |
132 | binding.btnYearly.setOnClickListener {
133 | iapConnector.subscribe(this, "yearly")
134 | }
135 | binding.btnQuite.setOnClickListener {
136 | iapConnector.purchase(this, "quite")
137 | }
138 | binding.btnModerate.setOnClickListener {
139 | iapConnector.purchase(this, "moderate")
140 | }
141 |
142 | binding.btnUltimate.setOnClickListener {
143 | iapConnector.purchase(this, "lifetime")
144 | }
145 | }
146 | else -> {
147 | binding.btPurchaseCons.isEnabled = false
148 | binding.btnMonthly.isEnabled = false
149 | binding.btnYearly.isEnabled = false
150 | binding.btnQuite.isEnabled = false
151 | binding.btnModerate.isEnabled = false
152 | binding.btnUltimate.isEnabled = false
153 | }
154 | }
155 | }
156 | }
157 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_achievement.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_bonus.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_earn.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_gift_box.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_github.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
13 |
16 |
19 |
22 |
25 |
28 |
31 |
34 |
37 |
40 |
43 |
46 |
49 |
52 |
55 |
58 |
61 |
64 |
67 |
70 |
73 |
76 |
79 |
82 |
85 |
88 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_lightning.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_money.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_money_bag.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_rich_man.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_trophy.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_wealth.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
20 |
21 |
36 |
37 |
47 |
48 |
53 |
54 |
62 |
63 |
68 |
69 |
70 |
71 |
72 |
81 |
82 |
87 |
88 |
96 |
97 |
102 |
103 |
104 |
105 |
106 |
107 |
118 |
119 |
124 |
125 |
133 |
134 |
139 |
140 |
141 |
142 |
143 |
144 |
154 |
155 |
160 |
161 |
169 |
170 |
175 |
176 |
177 |
178 |
179 |
180 |
191 |
192 |
197 |
198 |
206 |
207 |
212 |
213 |
214 |
215 |
216 |
217 |
227 |
228 |
233 |
234 |
242 |
243 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
265 |
266 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/top.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #0D0E13
4 | #000000
5 | #03DAC5
6 |
7 | #1A1A1A
8 | #FFFFFF
9 | #1A1A1A
10 |
11 | #7EC544
12 | #FBC233
13 | #F6404F
14 | #001c52
15 | #03DAC5
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/games_ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 1016011023550
5 |
6 | com.alphelios.superrich
7 |
8 | CgkIvsnn98gdEAIQAg
9 |
10 | CgkIvsnn98gdEAIQAw
11 |
12 | CgkIvsnn98gdEAIQBA
13 |
14 | CgkIvsnn98gdEAIQBQ
15 |
16 | CgkIvsnn98gdEAIQBg
17 |
18 | CgkIvsnn98gdEAIQBw
19 |
20 | CgkIvsnn98gdEAIQAQ
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #0D0E13
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Play Billing Library
3 | Subscribe
4 | Basic
5 | Moderate
6 | Quite
7 | Ultimate
8 | Github
9 | Home
10 | Achievements
11 | Leaderboard
12 | Monthly
13 | Yearly
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application) apply false
3 | alias(libs.plugins.android.library) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | }
6 |
7 | tasks.register("clean") {
8 | delete(layout.buildDirectory)
9 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## For more details on how to configure your build environment visit
2 | # http://www.gradle.org/docs/current/userguide/build_environment.html
3 | #
4 | # Specifies the JVM arguments used for the daemon process.
5 | # The setting is particularly useful for tweaking memory settings.
6 | # Default value: -Xmx1024m -XX:MaxPermSize=256m
7 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
8 | #
9 | # When configured, Gradle will run in incubating parallel mode.
10 | # This option should only be used with decoupled projects. More details, visit
11 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
12 | # org.gradle.parallel=true
13 | #Sun Apr 18 16:32:37 IST 2021
14 | android.useAndroidX=true
15 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
16 | android.nonTransitiveRClass=false
17 | android.nonFinalResIds=false
18 | org.gradle.configuration-cache=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "2.1.0"
3 | agp = "8.8.0"
4 | compileSdk = "34"
5 | targetSdk = "34"
6 | minSdk = "21"
7 | appcompat = "1.7.0"
8 | gridlayout = "1.0.0"
9 | material = "1.12.0"
10 | constraintlayout = "2.2.0"
11 | sdp-android = "1.1.1"
12 | billing-ktx = "7.1.1"
13 | lifecycle-extensions = "2.2.0"
14 | lifecycle-runtime-ktx = "2.8.7"
15 | junit = "4.13.2"
16 |
17 | [libraries]
18 | appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
19 | gridlayout = { module = "androidx.gridlayout:gridlayout", version.ref = "gridlayout" }
20 | material = { module = "com.google.android.material:material", version.ref = "material" }
21 | constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
22 | sdp-android = { module = "com.intuit.sdp:sdp-android", version.ref = "sdp-android" }
23 | billing-ktx = { module = "com.android.billingclient:billing-ktx", version.ref = "billing-ktx" }
24 | lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycle-extensions" }
25 | lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle-runtime-ktx" }
26 | junit = { module = "junit:junit", version.ref = "junit" }
27 |
28 | [plugins]
29 | android-application = { id = "com.android.application", version.ref = "agp" }
30 | android-library = { id = "com.android.library", version.ref = "agp" }
31 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/akshaaatt/Google-IAP/343fa87ab466932fead8c36b41493e76340b99f5/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jun 12 14:26:56 IST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/iap/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/iap/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.library)
3 | alias(libs.plugins.kotlin.android)
4 | id("maven-publish")
5 | }
6 |
7 | android {
8 | compileSdk = libs.versions.compileSdk.get().toInt()
9 |
10 | namespace = "com.limurse.iap"
11 | defaultConfig {
12 | minSdk = libs.versions.minSdk.get().toInt()
13 | }
14 |
15 | buildTypes {
16 | release {
17 | isMinifyEnabled = false
18 | proguardFiles("proguard-rules.pro")
19 | }
20 | }
21 |
22 | kotlin {
23 | jvmToolchain(17)
24 | }
25 |
26 | compileOptions {
27 | sourceCompatibility = JavaVersion.VERSION_17
28 | targetCompatibility = JavaVersion.VERSION_17
29 | }
30 |
31 | publishing {
32 | singleVariant("release") {
33 | withSourcesJar()
34 | withJavadocJar()
35 | }
36 | }
37 | }
38 |
39 | dependencies {
40 | implementation(libs.billing.ktx)
41 |
42 | implementation(libs.appcompat)
43 | implementation(libs.lifecycle.extensions)
44 | implementation(libs.lifecycle.runtime.ktx)
45 |
46 | testImplementation(libs.junit)
47 | }
48 |
49 | publishing {
50 | publications {
51 | create("release") {
52 | groupId = "com.limurse"
53 | artifactId = "Google-IAP"
54 | version = "1.8.0"
55 |
56 | afterEvaluate {
57 | from(components["release"])
58 | }
59 | }
60 | }
61 | }
--------------------------------------------------------------------------------
/iap/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/iap/src/main/java/com/limurse/iap/BillingClientConnectionListener.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iap
2 |
3 | interface BillingClientConnectionListener {
4 | fun onConnected(status: Boolean, billingResponseCode: Int)
5 | }
--------------------------------------------------------------------------------
/iap/src/main/java/com/limurse/iap/BillingService.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iap
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.net.Uri
7 | import android.util.Log
8 | import com.android.billingclient.api.*
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.launch
12 |
13 | class BillingService(
14 | private val context: Context,
15 | private val nonConsumableKeys: List,
16 | private val consumableKeys: List,
17 | private val subscriptionSkuKeys: List,
18 | ) : IBillingService(), PurchasesUpdatedListener, AcknowledgePurchaseResponseListener {
19 |
20 | private lateinit var mBillingClient: BillingClient
21 | private var decodedKey: String? = null
22 |
23 | private var enableDebug: Boolean = false
24 |
25 | private val productDetails = mutableMapOf()
26 |
27 | override fun init(key: String?) {
28 | decodedKey = key
29 | mBillingClient = BillingClient.newBuilder(context).setListener(this)
30 | .enablePendingPurchases().build()
31 | mBillingClient.startConnection(object : BillingClientStateListener{
32 | override fun onBillingServiceDisconnected() {
33 | log("onBillingServiceDisconnected")
34 | }
35 |
36 | override fun onBillingSetupFinished(billingResult: BillingResult) {
37 | log("onBillingSetupFinishedOkay: billingResult: $billingResult")
38 |
39 | when {
40 | billingResult.isOk() -> {
41 | isBillingClientConnected(true, billingResult.responseCode)
42 | nonConsumableKeys.queryProductDetails(BillingClient.ProductType.INAPP) {
43 | consumableKeys.queryProductDetails(BillingClient.ProductType.INAPP) {
44 | subscriptionSkuKeys.queryProductDetails(BillingClient.ProductType.SUBS) {
45 | CoroutineScope(Dispatchers.IO).launch {
46 | queryPurchases()
47 | }
48 | }
49 | }
50 | }
51 | }
52 | else -> {
53 | isBillingClientConnected(false, billingResult.responseCode)
54 | }
55 | }
56 | }
57 |
58 | })
59 | }
60 |
61 | /**
62 | * Query Google Play Billing for existing purchases.
63 | * New purchases will be provided to the PurchasesUpdatedListener.
64 | */
65 | private suspend fun queryPurchases() {
66 | val inAppResult: PurchasesResult = mBillingClient.queryPurchasesAsync(
67 | QueryPurchasesParams.newBuilder()
68 | .setProductType(BillingClient.ProductType.INAPP)
69 | .build()
70 | )
71 | processPurchases(inAppResult.purchasesList, isRestore = true)
72 | val subsResult: PurchasesResult = mBillingClient.queryPurchasesAsync(
73 | QueryPurchasesParams.newBuilder()
74 | .setProductType(BillingClient.ProductType.SUBS)
75 | .build()
76 | )
77 | processPurchases(subsResult.purchasesList, isRestore = true)
78 | }
79 |
80 | override fun buy(activity: Activity, sku: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) {
81 | if (!sku.isProductReady()) {
82 | log("buy. Google billing service is not ready yet. (SKU is not ready yet -1)")
83 | return
84 | }
85 |
86 | launchBillingFlow(activity, sku, BillingClient.ProductType.INAPP, null, obfuscatedAccountId, obfuscatedProfileId)
87 | }
88 |
89 | override fun subscribe(activity: Activity, sku: String, offerId: String?, obfuscatedAccountId: String?, obfuscatedProfileId: String?) {
90 | if (!sku.isProductReady()) {
91 | log("buy. Google billing service is not ready yet. (SKU is not ready yet -2)")
92 | return
93 | }
94 |
95 | launchBillingFlow(activity, sku, BillingClient.ProductType.SUBS, offerId, obfuscatedAccountId, obfuscatedProfileId)
96 | }
97 |
98 | private fun launchBillingFlow(activity: Activity, sku: String, type: String, offerId: String?, obfuscatedAccountId: String?, obfuscatedProfileId: String?) {
99 | sku.toProductDetails(type) { productDetails ->
100 | if (productDetails != null) {
101 |
102 | val productDetailsParamsList = mutableListOf()
103 | val builder = BillingFlowParams.ProductDetailsParams.newBuilder()
104 | .setProductDetails(productDetails)
105 |
106 | if(type == BillingClient.ProductType.SUBS) {
107 | var index = 0
108 | productDetails.subscriptionOfferDetails?.indexOfFirst { it.offerId == offerId }?.also {
109 | if (it >= 0) {
110 | index = it
111 | }
112 | }
113 | productDetails.subscriptionOfferDetails?.getOrNull(index)?.also {
114 | builder.setOfferToken(it.offerToken)
115 | }
116 | }
117 | productDetailsParamsList.add(builder.build())
118 | val billingFlowParamsBuilder = BillingFlowParams.newBuilder().setProductDetailsParamsList(productDetailsParamsList)
119 | if (obfuscatedAccountId != null) {
120 | billingFlowParamsBuilder.setObfuscatedAccountId(obfuscatedAccountId)
121 | }
122 | if (obfuscatedProfileId != null) {
123 | billingFlowParamsBuilder.setObfuscatedProfileId(obfuscatedProfileId)
124 | }
125 | val billingFlowParams = billingFlowParamsBuilder.build()
126 |
127 | mBillingClient.launchBillingFlow(activity, billingFlowParams)
128 | }
129 | }
130 | }
131 |
132 | override fun unsubscribe(activity: Activity, sku: String) {
133 | try {
134 | val intent = Intent()
135 | intent.action = Intent.ACTION_VIEW
136 | val subscriptionUrl = ("http://play.google.com/store/account/subscriptions"
137 | + "?package=" + activity.packageName
138 | + "&sku=" + sku)
139 | intent.data = Uri.parse(subscriptionUrl)
140 | activity.startActivity(intent)
141 | activity.finish()
142 | } catch (e: Exception) {
143 | Log.w(TAG, "Unsubscribing failed.")
144 | }
145 | }
146 |
147 | override fun enableDebugLogging(enable: Boolean) {
148 | this.enableDebug = enable
149 | }
150 |
151 | /**
152 | * Called by the Billing Library when new purchases are detected.
153 | */
154 | override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List?) {
155 | val responseCode = billingResult.responseCode
156 | val debugMessage = billingResult.debugMessage
157 | log("onPurchasesUpdated: responseCode:$responseCode debugMessage: $debugMessage")
158 | if (!billingResult.isOk()){
159 | updateFailedPurchases(purchases?.map { getPurchaseInfo(it) }, responseCode)
160 | }
161 | when (responseCode) {
162 | BillingClient.BillingResponseCode.OK -> {
163 | log("onPurchasesUpdated. purchase: $purchases")
164 | processPurchases(purchases)
165 | }
166 | BillingClient.BillingResponseCode.USER_CANCELED ->
167 | log("onPurchasesUpdated: User canceled the purchase")
168 | BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
169 | log("onPurchasesUpdated: The user already owns this item")
170 | //item already owned? call queryPurchases to verify and process all such items
171 | CoroutineScope(Dispatchers.IO).launch {
172 | queryPurchases()
173 | }
174 | }
175 | BillingClient.BillingResponseCode.DEVELOPER_ERROR ->
176 | Log.e(
177 | TAG, "onPurchasesUpdated: Developer error means that Google Play " +
178 | "does not recognize the configuration. If you are just getting started, " +
179 | "make sure you have configured the application correctly in the " +
180 | "Google Play Console. The SKU product ID must match and the APK you " +
181 | "are using must be signed with release keys."
182 | )
183 | }
184 | }
185 |
186 | private fun processPurchases(purchasesList: List?, isRestore: Boolean = false) {
187 | if (!purchasesList.isNullOrEmpty()) {
188 | log("processPurchases: " + purchasesList.size + " purchase(s)")
189 | purchases@ for (purchase in purchasesList) {
190 | // The purchase is considered successful in both PURCHASED and PENDING states.
191 | val purchaseSuccess = purchase.purchaseState == Purchase.PurchaseState.PURCHASED
192 | || purchase.purchaseState == Purchase.PurchaseState.PENDING
193 |
194 | if (purchaseSuccess && purchase.products[0].isProductReady()) {
195 | if (!isSignatureValid(purchase)) {
196 | log("processPurchases. Signature is not valid for: $purchase")
197 | updateFailedPurchase(getPurchaseInfo(purchase))
198 | continue@purchases
199 | }
200 |
201 | // Grant entitlement to the user.
202 | val productDetails = productDetails[purchase.products[0]]
203 | val isProductConsumable = consumableKeys.contains(purchase.products[0])
204 | when (productDetails?.productType) {
205 | BillingClient.ProductType.INAPP -> {
206 | // Consume the purchase
207 | when {
208 | isProductConsumable && purchase.purchaseState == Purchase.PurchaseState.PURCHASED -> {
209 | mBillingClient.consumeAsync(
210 | ConsumeParams.newBuilder()
211 | .setPurchaseToken(purchase.purchaseToken).build()
212 | ) { billingResult, _ ->
213 | when (billingResult.responseCode) {
214 | BillingClient.BillingResponseCode.OK -> {
215 | productOwned(getPurchaseInfo(purchase), false)
216 | }
217 | else -> {
218 | Log.d(
219 | TAG,
220 | "Handling consumables : Error during consumption attempt -> ${billingResult.debugMessage}"
221 | )
222 | updateFailedPurchase(getPurchaseInfo(purchase), billingResult.responseCode)
223 | }
224 | }
225 | }
226 | }
227 | else -> {
228 | productOwned(getPurchaseInfo(purchase), isRestore)
229 | }
230 | }
231 | }
232 | BillingClient.ProductType.SUBS -> {
233 | subscriptionOwned(getPurchaseInfo(purchase), isRestore)
234 | }
235 | }
236 |
237 | // If the state is PURCHASED, acknowledge the purchase if it hasn't been acknowledged yet.
238 | if (!purchase.isAcknowledged && !isProductConsumable && purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
239 | val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
240 | .setPurchaseToken(purchase.purchaseToken).build()
241 | mBillingClient.acknowledgePurchase(acknowledgePurchaseParams, this)
242 | }
243 | } else {
244 | Log.e(
245 | TAG, "processPurchases failed. purchase: $purchase " +
246 | "purchaseState: ${purchase.purchaseState} isSkuReady: ${purchase.products[0].isProductReady()}"
247 | )
248 | updateFailedPurchase(getPurchaseInfo(purchase))
249 | }
250 | }
251 | } else {
252 | log("processPurchases: with no purchases")
253 | }
254 | }
255 |
256 | private fun getPurchaseInfo(purchase: Purchase): DataWrappers.PurchaseInfo {
257 | return DataWrappers.PurchaseInfo(
258 | purchase.purchaseState,
259 | purchase.developerPayload,
260 | purchase.isAcknowledged,
261 | purchase.isAutoRenewing,
262 | purchase.orderId,
263 | purchase.originalJson,
264 | purchase.packageName,
265 | purchase.purchaseTime,
266 | purchase.purchaseToken,
267 | purchase.signature,
268 | purchase.products[0],
269 | purchase.accountIdentifiers
270 | )
271 | }
272 |
273 | private fun isSignatureValid(purchase: Purchase): Boolean {
274 | val key = decodedKey ?: return true
275 | return Security.verifyPurchase(key, purchase.originalJson, purchase.signature)
276 | }
277 |
278 | /**
279 | * Update Sku details after initialization.
280 | * This method has cache functionality.
281 | */
282 | private fun List.queryProductDetails(type: String, done: () -> Unit) {
283 | if (::mBillingClient.isInitialized.not() || !mBillingClient.isReady) {
284 | log("queryProductDetails. Google billing service is not ready yet.")
285 | done()
286 | return
287 | }
288 |
289 | if (this.isEmpty()) {
290 | log("queryProductDetails. Sku list is empty.")
291 | done()
292 | return
293 | }
294 |
295 | val productList = mutableListOf()
296 | this.forEach {
297 | productList.add(QueryProductDetailsParams.Product.newBuilder()
298 | .setProductId(it)
299 | .setProductType(type)
300 | .build())
301 | }
302 |
303 | val params = QueryProductDetailsParams.newBuilder().setProductList(productList)
304 |
305 | mBillingClient.queryProductDetailsAsync(params.build()) { billingResult, productDetailsList ->
306 | if (billingResult.isOk()) {
307 | isBillingClientConnected(true, billingResult.responseCode)
308 | productDetailsList.forEach {
309 | productDetails[it.productId] = it
310 | }
311 |
312 | productDetails.mapNotNull { entry ->
313 | entry.value?.let {
314 | when(it.productType){
315 | BillingClient.ProductType.SUBS->{
316 | entry.key to DataWrappers.ProductDetails(
317 | title = it.title,
318 | description = it.description,
319 | offers = it.subscriptionOfferDetails?.map { offerDetails ->
320 | DataWrappers.Offer(
321 | id = offerDetails.offerId,
322 | token = offerDetails.offerToken,
323 | tags = offerDetails.offerTags,
324 | pricingPhases = offerDetails.pricingPhases.pricingPhaseList.map { pricingPhase ->
325 | DataWrappers.PricingPhase(
326 | priceCurrencyCode = pricingPhase.priceCurrencyCode,
327 | price = pricingPhase.formattedPrice,
328 | priceAmount = pricingPhase.priceAmountMicros.div(1000000.0),
329 | billingCycleCount = pricingPhase.billingCycleCount,
330 | billingPeriod = pricingPhase.billingPeriod,
331 | recurrenceMode = pricingPhase.recurrenceMode
332 | )
333 | }
334 | )
335 | }
336 | )
337 | }
338 | else->{
339 | entry.key to DataWrappers.ProductDetails(
340 | title = it.title,
341 | description = it.description,
342 | offers = it.oneTimePurchaseOfferDetails?.let { offerDetails ->
343 | listOf(DataWrappers.Offer(
344 | id = null,
345 | token = null,
346 | tags = null,
347 | pricingPhases = listOf(
348 | DataWrappers.PricingPhase(
349 | priceCurrencyCode = offerDetails.priceCurrencyCode,
350 | price = offerDetails.formattedPrice,
351 | priceAmount = offerDetails.priceAmountMicros.div(1000000.0),
352 | billingCycleCount = null,
353 | billingPeriod = null,
354 | recurrenceMode = ProductDetails.RecurrenceMode.NON_RECURRING
355 | ))
356 | ))
357 | } ?: listOf()
358 | )
359 | }
360 | }
361 | }
362 | }.let {
363 | updatePrices(it.toMap())
364 | }
365 | }
366 | done()
367 | }
368 | }
369 |
370 | /**
371 | * Get Sku details by sku and type.
372 | * This method has cache functionality.
373 | */
374 | private fun String.toProductDetails(type: String, done: (productDetails: ProductDetails?) -> Unit = {}) {
375 | if (::mBillingClient.isInitialized.not() || !mBillingClient.isReady) {
376 | log("buy. Google billing service is not ready yet.(mBillingClient is not ready yet - 001)")
377 | done(null)
378 | return
379 | }
380 |
381 | val productDetailsCached = productDetails[this]
382 | if (productDetailsCached != null) {
383 | done(productDetailsCached)
384 | return
385 | }
386 |
387 | val productList = mutableListOf()
388 | this.forEach {
389 | productList.add(QueryProductDetailsParams.Product.newBuilder()
390 | .setProductId(it.toString())
391 | .setProductType(type)
392 | .build())
393 | }
394 |
395 | val params = QueryProductDetailsParams.newBuilder().setProductList(productList)
396 |
397 | mBillingClient.queryProductDetailsAsync(params.build()) { billingResult, productDetailsList ->
398 | when {
399 | billingResult.isOk() -> {
400 | isBillingClientConnected(true, billingResult.responseCode)
401 | val productDetails: ProductDetails? = productDetailsList.find { it.productId == this }
402 | // productDetails[this] = productDetails
403 | done(productDetails)
404 | }
405 | else -> {
406 | log("launchBillingFlow. Failed to get details for sku: $this")
407 | done(null)
408 | }
409 | }
410 | }
411 | }
412 |
413 | private fun String.isProductReady(): Boolean {
414 | return productDetails.containsKey(this) && productDetails[this] != null
415 | }
416 |
417 | override fun onAcknowledgePurchaseResponse(billingResult: BillingResult) {
418 | log("onAcknowledgePurchaseResponse: billingResult: $billingResult")
419 | if(!billingResult.isOk()){
420 | updateFailedPurchase(billingResponseCode = billingResult.responseCode)
421 | }
422 | }
423 |
424 | override fun close() {
425 | mBillingClient.endConnection()
426 | super.close()
427 | }
428 |
429 | private fun BillingResult.isOk(): Boolean {
430 | return this.responseCode == BillingClient.BillingResponseCode.OK
431 | }
432 |
433 | private fun log(message: String) {
434 | when {
435 | enableDebug -> {
436 | Log.d(TAG, message)
437 | }
438 | }
439 | }
440 |
441 | companion object {
442 | const val TAG = "GoogleBillingService"
443 | }
444 | }
--------------------------------------------------------------------------------
/iap/src/main/java/com/limurse/iap/BillingServiceListener.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iap
2 |
3 | interface BillingServiceListener {
4 | /**
5 | * Callback will be triggered upon obtaining information about product prices
6 | *
7 | * @param iapKeyPrices - a map with available products
8 | */
9 | fun onPricesUpdated(iapKeyPrices: Map)
10 |
11 | /**
12 | * Callback will be triggered when a purchase was failed.
13 | *
14 | * @param purchaseInfo - specifier of purchase info if exists
15 | * @param billingResponseCode - response code returned from the billing library if exists
16 | */
17 | fun onPurchaseFailed(purchaseInfo: DataWrappers.PurchaseInfo?, billingResponseCode: Int?)
18 | }
--------------------------------------------------------------------------------
/iap/src/main/java/com/limurse/iap/DataWrappers.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iap
2 |
3 | import com.android.billingclient.api.AccountIdentifiers
4 |
5 | class DataWrappers {
6 |
7 | data class ProductDetails(
8 | val title: String?,
9 | val description: String?,
10 | val offers: List?
11 | )
12 |
13 | data class PurchaseInfo(
14 | val purchaseState: Int,
15 | val developerPayload: String,
16 | val isAcknowledged: Boolean,
17 | val isAutoRenewing: Boolean,
18 | val orderId: String?,
19 | val originalJson: String,
20 | val packageName: String,
21 | val purchaseTime: Long,
22 | val purchaseToken: String,
23 | val signature: String,
24 | val sku: String,
25 | val accountIdentifiers: AccountIdentifiers?
26 | )
27 |
28 | data class Offer(
29 | val id: String?,
30 | val token: String?,
31 | val tags: List?,
32 | val pricingPhases: List
33 | )
34 |
35 | data class PricingPhase(
36 | val price: String?,
37 | val priceAmount: Double?,
38 | val priceCurrencyCode: String?,
39 | val billingCycleCount: Int?,
40 | val billingPeriod: String?,
41 | val recurrenceMode: Int?
42 | )
43 | }
--------------------------------------------------------------------------------
/iap/src/main/java/com/limurse/iap/IBillingService.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iap
2 |
3 | import android.app.Activity
4 | import android.os.Handler
5 | import android.os.Looper
6 | import androidx.annotation.CallSuper
7 |
8 | abstract class IBillingService {
9 |
10 | private val purchaseServiceListeners: MutableList = mutableListOf()
11 | private val subscriptionServiceListeners: MutableList = mutableListOf()
12 | private val billingClientConnectedListeners: MutableList = mutableListOf()
13 |
14 | fun addBillingClientConnectionListener(billingClientConnectionListener: BillingClientConnectionListener) {
15 | billingClientConnectedListeners.add(billingClientConnectionListener)
16 | }
17 |
18 | fun removeBillingClientConnectionListener(billingClientConnectionListener: BillingClientConnectionListener) {
19 | billingClientConnectedListeners.remove(billingClientConnectionListener)
20 | }
21 |
22 | fun addPurchaseListener(purchaseServiceListener: PurchaseServiceListener) {
23 | purchaseServiceListeners.add(purchaseServiceListener)
24 | }
25 |
26 | fun removePurchaseListener(purchaseServiceListener: PurchaseServiceListener) {
27 | purchaseServiceListeners.remove(purchaseServiceListener)
28 | }
29 |
30 | fun addSubscriptionListener(subscriptionServiceListener: SubscriptionServiceListener) {
31 | subscriptionServiceListeners.add(subscriptionServiceListener)
32 | }
33 |
34 | fun removeSubscriptionListener(subscriptionServiceListener: SubscriptionServiceListener) {
35 | subscriptionServiceListeners.remove(subscriptionServiceListener)
36 | }
37 |
38 | /**
39 | * @param purchaseInfo Product specifier
40 | * @param isRestore Flag indicating whether it's a fresh purchase or restored product
41 | */
42 | fun productOwned(purchaseInfo: DataWrappers.PurchaseInfo, isRestore: Boolean) {
43 | findUiHandler().post {
44 | productOwnedInternal(purchaseInfo, isRestore)
45 | }
46 | }
47 |
48 | private fun productOwnedInternal(purchaseInfo: DataWrappers.PurchaseInfo, isRestore: Boolean) {
49 | for (purchaseServiceListener in purchaseServiceListeners) {
50 | if (isRestore) {
51 | purchaseServiceListener.onProductRestored(purchaseInfo)
52 | } else {
53 | purchaseServiceListener.onProductPurchased(purchaseInfo)
54 | }
55 | }
56 | }
57 |
58 | /**
59 | * @param purchaseInfo Subscription specifier
60 | * @param isRestore Flag indicating whether it's a fresh purchase or restored subscription
61 | */
62 | fun subscriptionOwned(purchaseInfo: DataWrappers.PurchaseInfo, isRestore: Boolean) {
63 | findUiHandler().post {
64 | subscriptionOwnedInternal(purchaseInfo, isRestore)
65 | }
66 | }
67 |
68 | private fun subscriptionOwnedInternal(purchaseInfo: DataWrappers.PurchaseInfo, isRestore: Boolean) {
69 | for (subscriptionServiceListener in subscriptionServiceListeners) {
70 | if (isRestore) {
71 | subscriptionServiceListener.onSubscriptionRestored(purchaseInfo)
72 | } else {
73 | subscriptionServiceListener.onSubscriptionPurchased(purchaseInfo)
74 | }
75 | }
76 | }
77 |
78 | fun isBillingClientConnected(status: Boolean, responseCode: Int) {
79 | findUiHandler().post {
80 | for (billingServiceListener in billingClientConnectedListeners) {
81 | billingServiceListener.onConnected(status, responseCode)
82 | }
83 | }
84 | }
85 |
86 | fun updatePrices(iapKeyPrices: Map) {
87 | findUiHandler().post {
88 | updatePricesInternal(iapKeyPrices)
89 | }
90 | }
91 |
92 | private fun updatePricesInternal(iapKeyPrices: Map) {
93 | for (billingServiceListener in purchaseServiceListeners) {
94 | billingServiceListener.onPricesUpdated(iapKeyPrices)
95 | }
96 | for (billingServiceListener in subscriptionServiceListeners) {
97 | billingServiceListener.onPricesUpdated(iapKeyPrices)
98 | }
99 | }
100 |
101 | fun updateFailedPurchases(purchaseInfo: List? = null, billingResponseCode: Int? = null) {
102 | purchaseInfo?.forEach {
103 | updateFailedPurchase(it, billingResponseCode)
104 | } ?: updateFailedPurchase()
105 | }
106 |
107 | fun updateFailedPurchase(purchaseInfo: DataWrappers.PurchaseInfo? = null, billingResponseCode: Int? = null) {
108 | findUiHandler().post {
109 | updateFailedPurchasesInternal(purchaseInfo, billingResponseCode)
110 | }
111 | }
112 |
113 | private fun updateFailedPurchasesInternal(purchaseInfo: DataWrappers.PurchaseInfo? = null, billingResponseCode: Int? = null) {
114 | for (billingServiceListener in purchaseServiceListeners) {
115 | billingServiceListener.onPurchaseFailed(purchaseInfo, billingResponseCode)
116 | }
117 | for (billingServiceListener in subscriptionServiceListeners) {
118 | billingServiceListener.onPurchaseFailed(purchaseInfo, billingResponseCode)
119 | }
120 | }
121 |
122 | abstract fun init(key: String?)
123 | abstract fun buy(activity: Activity, sku: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?)
124 | abstract fun subscribe(activity: Activity, sku: String, offerId: String?, obfuscatedAccountId: String?, obfuscatedProfileId: String?)
125 | abstract fun unsubscribe(activity: Activity, sku: String)
126 | abstract fun enableDebugLogging(enable: Boolean)
127 |
128 | @CallSuper
129 | open fun close() {
130 | subscriptionServiceListeners.clear()
131 | purchaseServiceListeners.clear()
132 | billingClientConnectedListeners.clear()
133 | }
134 | }
135 |
136 | fun findUiHandler(): Handler {
137 | return Handler(Looper.getMainLooper())
138 | }
--------------------------------------------------------------------------------
/iap/src/main/java/com/limurse/iap/IapConnector.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iap
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import kotlinx.coroutines.DelicateCoroutinesApi
6 |
7 | /**
8 | * Initialize billing service.
9 | *
10 | * @param context Application context.
11 | * @param nonConsumableKeys SKU list for non-consumable one-time products.
12 | * @param consumableKeys SKU list for consumable one-time products.
13 | * @param subscriptionKeys SKU list for subscriptions.
14 | * @param key Key to verify purchase messages. Leave it empty if you want to skip verification.
15 | * @param enableLogging Log operations/errors to the logcat for debugging purposes.
16 | */
17 | @OptIn(DelicateCoroutinesApi::class)
18 | class IapConnector @JvmOverloads constructor(
19 | context: Context,
20 | nonConsumableKeys: List = emptyList(),
21 | consumableKeys: List = emptyList(),
22 | subscriptionKeys: List = emptyList(),
23 | key: String? = null,
24 | enableLogging: Boolean = false
25 | ) {
26 |
27 | private var mBillingService: IBillingService? = null
28 |
29 | init {
30 | val contextLocal = context.applicationContext ?: context
31 | mBillingService = BillingService(contextLocal, nonConsumableKeys, consumableKeys, subscriptionKeys)
32 | getBillingService().init(key)
33 | getBillingService().enableDebugLogging(enableLogging)
34 | }
35 |
36 | fun addBillingClientConnectionListener(billingClientConnectionListener: BillingClientConnectionListener) {
37 | getBillingService().addBillingClientConnectionListener(billingClientConnectionListener)
38 | }
39 |
40 | fun removeBillingClientConnectionListener(billingClientConnectionListener: BillingClientConnectionListener) {
41 | getBillingService().removeBillingClientConnectionListener(billingClientConnectionListener)
42 | }
43 |
44 | fun addPurchaseListener(purchaseServiceListener: PurchaseServiceListener) {
45 | getBillingService().addPurchaseListener(purchaseServiceListener)
46 | }
47 |
48 | fun removePurchaseListener(purchaseServiceListener: PurchaseServiceListener) {
49 | getBillingService().removePurchaseListener(purchaseServiceListener)
50 | }
51 |
52 | fun addSubscriptionListener(subscriptionServiceListener: SubscriptionServiceListener) {
53 | getBillingService().addSubscriptionListener(subscriptionServiceListener)
54 | }
55 |
56 | fun removeSubscriptionListener(subscriptionServiceListener: SubscriptionServiceListener) {
57 | getBillingService().removeSubscriptionListener(subscriptionServiceListener)
58 | }
59 |
60 | fun purchase(activity: Activity, sku: String, obfuscatedAccountId: String? = null, obfuscatedProfileId: String? = null) {
61 | getBillingService().buy(activity, sku, obfuscatedAccountId, obfuscatedProfileId)
62 | }
63 |
64 | fun subscribe(activity: Activity, sku: String, offerId: String? = null,
65 | obfuscatedAccountId: String? = null, obfuscatedProfileId: String? = null) {
66 | getBillingService().subscribe(activity, sku, offerId, obfuscatedAccountId, obfuscatedProfileId)
67 | }
68 |
69 | fun unsubscribe(activity: Activity, sku: String) {
70 | getBillingService().unsubscribe(activity, sku)
71 | }
72 |
73 | fun destroy() {
74 | getBillingService().close()
75 | }
76 |
77 | private fun getBillingService(): IBillingService {
78 | return mBillingService ?: let {
79 | throw RuntimeException("Call IapConnector to initialize billing service")
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/iap/src/main/java/com/limurse/iap/PurchaseServiceListener.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iap
2 |
3 | interface PurchaseServiceListener : BillingServiceListener {
4 | /**
5 | * Callback will be triggered upon obtaining information about product prices
6 | *
7 | * @param iapKeyPrices - a map with available products
8 | */
9 | override fun onPricesUpdated(iapKeyPrices: Map)
10 |
11 | /**
12 | * Callback will be triggered when a product purchased successfully
13 | *
14 | * @param purchaseInfo - specifier of owned product
15 | */
16 | fun onProductPurchased(purchaseInfo: DataWrappers.PurchaseInfo)
17 |
18 | /**
19 | * Callback will be triggered upon owned products restore
20 | *
21 | * @param purchaseInfo - specifier of owned product
22 | */
23 | fun onProductRestored(purchaseInfo: DataWrappers.PurchaseInfo)
24 | }
--------------------------------------------------------------------------------
/iap/src/main/java/com/limurse/iap/Security.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iap
2 |
3 | import android.text.TextUtils
4 | import android.util.Base64
5 | import android.util.Log
6 | import java.io.IOException
7 | import java.security.InvalidKeyException
8 | import java.security.KeyFactory
9 | import java.security.NoSuchAlgorithmException
10 | import java.security.PublicKey
11 | import java.security.Signature
12 | import java.security.SignatureException
13 | import java.security.spec.InvalidKeySpecException
14 | import java.security.spec.X509EncodedKeySpec
15 |
16 | /**
17 | * Security-related methods. For a secure implementation, all of this code should be implemented on
18 | * a server that communicates with the application on the device.
19 | */
20 | object Security {
21 | private const val TAG = "IABUtil/Security"
22 | private const val KEY_FACTORY_ALGORITHM = "RSA"
23 | private const val SIGNATURE_ALGORITHM = "SHA1withRSA"
24 |
25 | /**
26 | * Verifies that the data was signed with the given signature
27 | *
28 | * @param base64PublicKey the base64-encoded public key to use for verifying.
29 | * @param signedData the signed JSON string (signed, not encrypted)
30 | * @param signature the signature for the data, signed with the private key
31 | * @throws IOException if encoding algorithm is not supported or key specification
32 | * is invalid
33 | */
34 | @Throws(IOException::class)
35 | fun verifyPurchase(base64PublicKey: String, signedData: String, signature: String): Boolean {
36 | if ((TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey)
37 | || TextUtils.isEmpty(signature))
38 | ) {
39 | Log.w(TAG, "Purchase verification failed: missing data.")
40 | return false
41 | }
42 | val key = generatePublicKey(base64PublicKey)
43 | return verify(key, signedData, signature)
44 | }
45 |
46 | /**
47 | * Generates a PublicKey instance from a string containing the Base64-encoded public key.
48 | *
49 | * @param encodedPublicKey Base64-encoded public key
50 | * @throws IOException if encoding algorithm is not supported or key specification
51 | * is invalid
52 | */
53 | @Throws(IOException::class)
54 | private fun generatePublicKey(encodedPublicKey: String): PublicKey {
55 | try {
56 | val decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT)
57 | val keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM)
58 | return keyFactory.generatePublic(X509EncodedKeySpec(decodedKey))
59 | } catch (e: NoSuchAlgorithmException) {
60 | // "RSA" is guaranteed to be available.
61 | throw RuntimeException(e)
62 | } catch (e: InvalidKeySpecException) {
63 | val msg = "Invalid key specification: $e"
64 | Log.w(TAG, msg)
65 | throw IOException(msg)
66 | }
67 | }
68 |
69 | /**
70 | * Verifies that the signature from the server matches the computed signature on the data.
71 | * Returns true if the data is correctly signed.
72 | *
73 | * @param publicKey public key associated with the developer account
74 | * @param signedData signed data from server
75 | * @param signature server signature
76 | * @return true if the data and signature match
77 | */
78 | private fun verify(publicKey: PublicKey, signedData: String, signature: String): Boolean {
79 | val signatureBytes: ByteArray
80 | try {
81 | signatureBytes = Base64.decode(signature, Base64.DEFAULT)
82 | } catch (e: IllegalArgumentException) {
83 | Log.w(TAG, "Base64 decoding failed.")
84 | return false
85 | }
86 | try {
87 | val signatureAlgorithm = Signature.getInstance(SIGNATURE_ALGORITHM)
88 | signatureAlgorithm.initVerify(publicKey)
89 | signatureAlgorithm.update(signedData.toByteArray())
90 | if (!signatureAlgorithm.verify(signatureBytes)) {
91 | Log.w(TAG, "Signature verification failed...")
92 | return false
93 | }
94 | return true
95 | } catch (e: NoSuchAlgorithmException) {
96 | // "RSA" is guaranteed to be available.
97 | throw RuntimeException(e)
98 | } catch (e: InvalidKeyException) {
99 | Log.w(TAG, "Invalid key specification.")
100 | } catch (e: SignatureException) {
101 | Log.w(TAG, "Signature exception.")
102 | }
103 | return false
104 | }
105 | }
--------------------------------------------------------------------------------
/iap/src/main/java/com/limurse/iap/SubscriptionServiceListener.kt:
--------------------------------------------------------------------------------
1 | package com.limurse.iap
2 |
3 | interface SubscriptionServiceListener : BillingServiceListener {
4 | /**
5 | * Callback will be triggered upon owned subscription restore
6 | *
7 | * @param purchaseInfo - specifier of owned subscription
8 | */
9 | fun onSubscriptionRestored(purchaseInfo: DataWrappers.PurchaseInfo)
10 |
11 | /**
12 | * Callback will be triggered when a subscription purchased successfully
13 | *
14 | * @param purchaseInfo - specifier of purchased subscription
15 | */
16 | fun onSubscriptionPurchased(purchaseInfo: DataWrappers.PurchaseInfo)
17 | }
--------------------------------------------------------------------------------
/jitpack.yml:
--------------------------------------------------------------------------------
1 | jdk:
2 | - openjdk17
--------------------------------------------------------------------------------
/keystore.properties:
--------------------------------------------------------------------------------
1 | licenseKey=test
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
11 | repositories {
12 | google()
13 | mavenCentral()
14 | }
15 | }
16 |
17 | include(":app")
18 | include(":iap")
--------------------------------------------------------------------------------