Skip to content

Commit

Permalink
Merge pull request #116 from XRPL-Labs/develop
Browse files Browse the repository at this point in the history
v3.1.0
  • Loading branch information
N3TC4T authored Nov 18, 2024
2 parents cfe7faf + b1a4ae4 commit 9f51877
Show file tree
Hide file tree
Showing 292 changed files with 11,356 additions and 3,935 deletions.
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ apply plugin: "com.google.firebase.crashlytics"

import com.android.build.OutputFile

def canonicalVersionName = "3.0.1"
def canonicalVersionCode = 19
def canonicalVersionName = "3.1.0"
def canonicalVersionCode = 29

// NOTE: DO NOT change postFixSize value, this is for handling legacy method for handling the versioning in android
def postFixSize = 30_000
Expand Down
69 changes: 36 additions & 33 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,15 @@
<application
android:name=".ApplicationLoader"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:resizeableActivity="false"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme"

tools:targetApi="34">
<activity
android:name=".LaunchActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
Expand All @@ -87,38 +91,37 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="xumm.app"
android:pathPrefix="/detect"
android:scheme="https" />
<data
android:host="xumm.app"
android:pathPrefix="/tx"
android:scheme="https" />
<data
android:host="xumm.app"
android:pathPrefix="/pair"
android:scheme="https" />
<data
android:host="xumm.app"
android:pathPrefix="/sign"
android:scheme="https" />
<data
android:host="xaman.app"
android:pathPrefix="/detect"
android:scheme="https" />
<data
android:host="xaman.app"
android:pathPrefix="/tx"
android:scheme="https" />
<data
android:host="xaman.app"
android:pathPrefix="/pair"
android:scheme="https" />
<data
android:host="xaman.app"
android:pathPrefix="/sign"
android:scheme="https" />
<data android:scheme="https" />
<data android:host="xumm.app" />
<data android:pathPrefix="/detect" />

<data android:scheme="https" />
<data android:host="xumm.app" />
<data android:pathPrefix="/tx" />

<data android:scheme="https" />
<data android:host="xumm.app" />
<data android:pathPrefix="/pair" />

<data android:scheme="https" />
<data android:host="xumm.app" />
<data android:pathPrefix="/sign" />

<data android:scheme="https" />
<data android:host="xaman.app" />
<data android:pathPrefix="/detect" />

<data android:scheme="https" />
<data android:host="xaman.app" />
<data android:pathPrefix="/tx" />

<data android:scheme="https" />
<data android:host="xaman.app" />
<data android:pathPrefix="/pair" />

<data android:scheme="https" />
<data android:host="xaman.app" />
<data android:pathPrefix="/sign" />
</intent-filter>

<intent-filter>
Expand Down
146 changes: 96 additions & 50 deletions android/app/src/main/java/libs/common/InAppPurchaseModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
import java.util.List;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

import android.os.Handler;
import android.os.Looper;

import android.util.Log;

import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.PendingPurchasesParams;
Expand Down Expand Up @@ -43,6 +46,7 @@ public class InAppPurchaseModule extends ReactContextBaseJavaModule implements P
public static final String TAG = NAME;

private static final String E_CLIENT_IS_NOT_READY = "E_CLIENT_IS_NOT_READY";
private static final String E_PRODUCT_DETAILS_NOT_FOUND = "E_PRODUCT_DETAILS_NOT_FOUND";
private static final String E_PRODUCT_IS_NOT_AVAILABLE = "E_PRODUCT_IS_NOT_AVAILABLE";
private static final String E_NO_PENDING_PURCHASE = "E_NO_PENDING_PURCHASE";
private static final String E_PURCHASE_CANCELED = "E_PURCHASE_CANCELED";
Expand All @@ -55,6 +59,8 @@ public class InAppPurchaseModule extends ReactContextBaseJavaModule implements P
private final HashMap<String, ProductDetails> productDetailsHashMap = new HashMap<>();
private final BillingClient billingClient;
private final GoogleApiAvailability googleApiAvailability;
private final AtomicBoolean isUserPurchasing = new AtomicBoolean(false);
private final Handler handler = new Handler(Looper.getMainLooper());

private Promise billingFlowPromise;

Expand All @@ -72,12 +78,6 @@ public class InAppPurchaseModule extends ReactContextBaseJavaModule implements P
googleApiAvailability = GoogleApiAvailability.getInstance();
}

@NonNull
@Override
public String getName() {
return NAME;
}


//---------------------------------
// React methods - These methods are exposed to React JS
Expand All @@ -89,6 +89,17 @@ public String getName() {
//
//---------------------------------

@ReactMethod(isBlockingSynchronousMethod = true)
public boolean isUserPurchasing() {
return isUserPurchasing.get();
}


@NonNull
@Override
public String getName() {
return NAME;
}

// Starts a connection with Google BillingClient
@ReactMethod
Expand Down Expand Up @@ -117,15 +128,55 @@ public void onBillingSetupFinished(@NonNull BillingResult billingResult) {

@Override
public void onBillingServiceDisconnected() {
promise.reject(E_CLIENT_IS_NOT_READY, "billing client service disconnected");
promise.reject(E_CLIENT_IS_NOT_READY, "Billing client service disconnected!");
}
});
}

@ReactMethod
public void getProductDetails(String productId, Promise promise) {
if (!isReady()) {
promise.reject(E_CLIENT_IS_NOT_READY, "Billing client is not ready, forgot to initialize?");
return;
}


// try to fetch cached version of product details
if (productDetailsHashMap.containsKey(productId)) {
promise.resolve(this.productToJson(Objects.requireNonNull(productDetailsHashMap.get(productId))));
return;
}

// no cached product details
// fetch product details
ImmutableList<QueryProductDetailsParams.Product> productList = ImmutableList.of(QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(BillingClient.ProductType.INAPP)
.build());

QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();

billingClient.queryProductDetailsAsync(queryProductDetailsParams, (billingResult, list) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && !list.isEmpty()) {
ProductDetails productDetails = list.get(0);

// cache the product details
productDetailsHashMap.put(list.get(0).getProductId(), productDetails);

// Launch the billing flow
promise.resolve(this.productToJson(productDetails));
} else {
promise.reject(E_PRODUCT_IS_NOT_AVAILABLE, "Unable to load the product details with productId " + productId);
}
});
}

@ReactMethod
public void restorePurchases(Promise promise) {
if (!isReady()) {
promise.reject(E_CLIENT_IS_NOT_READY, "billing client is not ready, forgot to initialize?");
promise.reject(E_CLIENT_IS_NOT_READY, "Billing client is not ready, forgot to initialize?");
return;
}

Expand All @@ -152,7 +203,7 @@ public void restorePurchases(Promise promise) {
@ReactMethod
public void purchase(String productId, Promise promise) {
if (!isReady()) {
promise.reject(E_CLIENT_IS_NOT_READY, "billingClient is not ready, forgot to initialize?");
promise.reject(E_CLIENT_IS_NOT_READY, "Billing client is not ready, forgot to initialize?");
return;
}

Expand All @@ -162,34 +213,14 @@ public void purchase(String productId, Promise promise) {
return;
}

// no cached product details
// fetch product details
ImmutableList<QueryProductDetailsParams.Product> productList = ImmutableList.of(QueryProductDetailsParams.Product.newBuilder()
.setProductId(productId)
.setProductType(BillingClient.ProductType.INAPP)
.build());

QueryProductDetailsParams queryProductDetailsParams = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build();

billingClient.queryProductDetailsAsync(queryProductDetailsParams, (billingResult, list) -> {
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK && list.size() > 0) {
ProductDetails productDetails = list.get(0);

// cache the product details
productDetailsHashMap.put(list.get(0).getProductId(), productDetails);

// Launch the billing flow
launchBillingFlow(productDetails, promise);
} else {
Log.e(TAG, "Unable to load the product details with productId " + productId + " getResponseCode:" + billingResult.getResponseCode() + " list.size:" + list.size());
promise.reject(E_PRODUCT_IS_NOT_AVAILABLE, "Unable to load the product details with productId " + productId);
}
});
promise.reject(E_PRODUCT_DETAILS_NOT_FOUND, "Product is unavailable!");
}


//---------------------------------
// Private methods
//---------------------------------

// Consumes a purchase token, indicating that the product has been provided to the user.
@ReactMethod
public void finalizePurchase(String purchaseToken, Promise promise) {
Expand All @@ -199,17 +230,11 @@ public void finalizePurchase(String purchaseToken, Promise promise) {
if (billing.getResponseCode() == BillingClient.BillingResponseCode.OK) {
promise.resolve(purchaseToken);
} else {
promise.reject(E_FINISH_TRANSACTION_FAILED, "billing response code " + billing.getResponseCode());
promise.reject(E_FINISH_TRANSACTION_FAILED, "Finalize purchase failed: code " + billing.getResponseCode());
}
});
}


//---------------------------------
// Private methods
//---------------------------------


// This method gets called when purchases are updated.
@Override
public void onPurchasesUpdated(@NonNull BillingResult billing, @Nullable List<Purchase> list) {
Expand All @@ -218,16 +243,22 @@ public void onPurchasesUpdated(@NonNull BillingResult billing, @Nullable List<Pu
return;
}

// Delay setting isUserPurchasing to false by 1 sec
// This is required due AppState change trigger after IAP activity launch
handler.postDelayed(() -> isUserPurchasing.set(false), 1000);


// something went wrong with the purchase, reject the billingFlowPromise
if (billing.getResponseCode() != BillingClient.BillingResponseCode.OK) {
switch (billing.getResponseCode()) {
case BillingClient.BillingResponseCode.USER_CANCELED:
rejectBillingFlowPromise(E_PURCHASE_CANCELED, "purchase canceled by users");
rejectBillingFlowPromise(E_PURCHASE_CANCELED, "The purchase was canceled by the user.");
break;
case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED:
rejectBillingFlowPromise(E_ALREADY_PURCHASED, "item already owned by users");
rejectBillingFlowPromise(E_ALREADY_PURCHASED, "The item is already owned.");
break;
default:
rejectBillingFlowPromise(E_PURCHASE_FAILED, "billing response code " + billing.getResponseCode());
rejectBillingFlowPromise(E_PURCHASE_FAILED, "An unexpected error occurred, code: " + billing.getResponseCode());
break;
}
return;
Expand All @@ -236,7 +267,7 @@ public void onPurchasesUpdated(@NonNull BillingResult billing, @Nullable List<Pu

// something is not right?
if (list == null || list.isEmpty()) {
rejectBillingFlowPromise(E_PURCHASE_FAILED, "the purchase list is empty!");
rejectBillingFlowPromise(E_PURCHASE_FAILED, "The purchase list is empty!");
return;
}

Expand All @@ -262,10 +293,16 @@ public boolean isReady() {
// A private method to actually launch the billing flow once we've loaded the product details.
// This method may fail and reject the promise if there's already a billing flow in progress.
private void launchBillingFlow(ProductDetails productDetails, Promise promise) {
// set flag
if (isUserPurchasing.getAndSet(true)) {
promise.reject(E_PURCHASE_FAILED, "There is a Billing flow going on at the moment, can't start a new one!");
return;
}

// if we already have an promise going on for billing flow, reject it as we don't want to start
// the new flow without closing the old one
if (billingFlowPromise != null) {
promise.reject(E_PURCHASE_FAILED, "There is a Billing flow going on at the moment, can't start the new one!");
promise.reject(E_PURCHASE_FAILED, "There is a Billing flow going on at the moment, can't start a new one!");
return;
}

Expand Down Expand Up @@ -295,23 +332,32 @@ private WritableMap purchaseToMap(Purchase purchase) {
return purchaseMap;
}

private WritableMap productToJson(ProductDetails productDetails) {
final WritableMap results = Arguments.createMap();
results.putString("title", productDetails.getTitle());
results.putString("description", productDetails.getDescription());
results.putString("price", Objects.requireNonNull(productDetails.getOneTimePurchaseOfferDetails()).getFormattedPrice());
results.putString("productId", productDetails.getProductId());

return results;
}


private void resolveBillingFlowPromise(final WritableArray productsToBeConsumed) {
try {
billingFlowPromise.resolve(productsToBeConsumed);
billingFlowPromise = null;
} catch (Exception error) {
Log.e(TAG, "resolveBillingFlowPromise: " + error.getMessage());
// ignore
}
}

private void rejectBillingFlowPromise(final String code, final String message) {
try {
billingFlowPromise.reject(code, message);
Log.d(TAG, "rejectBillingFlowPromise " + code);
billingFlowPromise = null;
} catch (Exception error) {
Log.e(TAG, "rejectBillingFlowPromise: " + error.getMessage());
// ignore
}
}
}
Expand Down
Loading

0 comments on commit 9f51877

Please sign in to comment.