How to successfully complete an in app purchase using flutter on android? - android

What I want
I want to integrate Google Play in app purchases into my flutter app, the product I want to sell is a consumable. Right now I am using the in_app_purchase: (0.3.4+16) package.
What I did
Create the product in the google play developer console
Set up the project according to the documentation of the in_app_purchase package
Implemented some logic to buy a consumable item (see below)
Built an alpha release and uploaded it to the alpha test track in order to test it
Created a new google account on my developer phone, registered it as tester and downloaded the alpha version
Purchased with a "test card, always approves"
Purchased with my PayPal account
Expected and actual result
I expect the payment to work and all api calls to return ok.
When I initiate a purchase on my phone the purchase flow starts and I can select the desired payment method. After I accept the payment the _listenToPurchaseUpdated(...) method is called, as expected.
However, the call to InAppPurchaseConnection.instance.completePurchase(p) returns a BillingResponse.developerError and I get the following debug messages:
W/BillingHelper: Couldn't find purchase lists, trying to find single data.
I/flutter: result: BillingResponse.developerError (Purchase is in an invalid state.)
This error comes with the "test card, always approves" and also when I start a real transaction using PayPal. For the PayPal purchase I got a confirmation Email, that the transaction was successful.
In the documentation it says:
Warning! Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android.
Summarized question
How can I get the call to InAppPurchaseConnection.instance.completePurchase(p) to return a successful result?
The purchase implementation
The code to setup in app purchases is implemented as shown in the documentation:
InAppPurchaseConnection.enablePendingPurchases();
Stream<List<PurchaseDetails>> purchaseUpdated = InAppPurchaseConnection.instance.purchaseUpdatedStream;
_subscription = purchaseUpdated.listen(_listenToPurchaseUpdated, onDone: () {
_subscription.cancel();
}, onError: (error) {
// handle error here.
});
...
Future<void> _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) async {
for (var p in purchaseDetailsList) {
// Code to validate the payment
if (!p.pendingCompletePurchase) continue;
var result = await InAppPurchaseConnection.instance.completePurchase(p);
if (result.responseCode != BillingResponse.ok) {
print("result: ${result.responseCode} (${result.debugMessage})");
}
}
}
To buy a consumable I have this method which queries the product details and calls buyConsumable(...)
Future<bool> _buyConsumableById(String id) async {
final ProductDetailsResponse response = await InAppPurchaseConnection
.instance
.queryProductDetails([id].toSet());
if (response.notFoundIDs.isNotEmpty || response.productDetails.isEmpty) {
return false;
}
List<ProductDetails> productDetails = response.productDetails;
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: productDetails[0],
);
return await InAppPurchaseConnection.instance.buyConsumable(
purchaseParam: purchaseParam,
);
}

The solution is to not call the completePurchase(...) method for consumable purchases. By default the library consumes the purchase for you which implicitly acts as a call to completePurchase(...).
Background
The call to InAppPurchaseConnection.instance.buyConsumable(...) has an optional boolean parameter autoConsume which is always true. This means that, on android, the purchase is consumed right before the callback to the purchaseUpdatedStream.
The documentation of the completePurchase method says the following:
The [consumePurchase] acts as an implicit [completePurchase] on Android
Code to fix the problem
Future<void> _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) async {
for (var p in purchaseDetailsList) {
// Code to validate the payment
if (!p.pendingCompletePurchase) continue;
if (_isConsumable(p.productID)) continue; // Determine if the item is consumable. If so do not consume it
var result = await InAppPurchaseConnection.instance.completePurchase(p);
if (result.responseCode != BillingResponse.ok) {
print("result: ${result.responseCode} (${result.debugMessage})");
}
}
}

Related

(ANDROID) Firebase Google SignIn - How to logout from all devices?

I want to log out from all devices on the button click for Firebase google login. Is this possible? If yes please share the doc or sample code.
Couldn't find any solution for this on stackoverflow.
Code for LogOut from current device
FirebaseAuth.getInstance().signOut()
This works only for current device & not all devices.
To revoke the refresh tokens for a given user (which will prevent clients requesting a new ID token), you can:
Change the user's password
Disable the user using a Firebase Server SDK and updating the disabled property. For Node, you would use the Auth#updateUser method of firebase-admin.
Revoke the tokens manually using a Firebase Server SDK. For Node, you would use the Auth#revokeRefreshTokens() method of firebase-admin.
It's important to note here, that these methods won't invalidate already issued tokens. These tokens will be considered valid up to as long as an hour unless you explicitly test if they have been revoked. You can deal with this somewhat by using FirebaseUser#getIdToken(true) when you detect the user has opened the app but is already signed in. This method tries to get a fresh ID token rather than just use the cached ID token - just make sure to handle the case where the refresh token has been invalidated.
Of the above options, I would recommend the last option and make it available using a Callable Cloud Function.
The server-side code, hosted on Cloud Functions:
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
export const revokeRefreshTokens = functions.https.onCall((data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError(
"failed-precondition",
"The function must be called while authenticated."
);
}
// New optional feature: Firebase App Check
if (context.app === undefined) {
throw new functions.https.HttpsError(
"failed-precondition",
"The function must be called from an App Check verified app."
);
}
const uid = context.auth.uid;
return admin
.auth()
.revokeRefreshTokens(uid)
.then(() => {
return admin.auth().getUser(uid);
})
.then((userRecord) => {
// send back a response with the UID and time tokens were revoked
const timestamp = new Date(userRecord.tokensValidAfterTime).getTime();
return { uid, revokedAt: timestamp }
})
.catch((err) => {
// rethrow any errors as HttpsError for clients
throw new functions.https.HttpsError("unknown", err.code || err.message);
});
});
The client-side code:
public void revokeRefreshTokens() {
return FirebaseFunctions.getInstance()
.getHttpsCallable("revokeRefreshTokens")
.call(data)
.continueWith(new Continuation<HttpsCallableResult, Void>() {
#Override
public String then(#NonNull Task<HttpsCallableResult> task) throws Exception {
if (task.isSuccessful()) {
// revoked successfully, end this sign-in session
FirebaseAuth.getInstance().signOut()
} else {
// failed to revoke, rethrow error
throw task.getException();
}
}
});
}

How to add in app purchase to react native app?

So paid pages in my app, when a user click to open any of these pages i want to run a function to check if the product is purchased and if it's not then buy the product, I have found two packages which are react-native-iap and expo-in-app-purchase but did not find clear instructions on how to achieve the process i need..
const paidPage = async (productId , navTo) => {
// PASS THE PRODUCT ID AND A FUNCTION THAT NAVIGATE TO THE PAGE
// HERE I WANT TO CHECK IF THE PRODUCT IS PURCHASED, IF YES THEN NAVIGATE TO THE PAGE
// IF NO THEN PURCHASE THE PRODUCT
}
You have to check if you have already bought the product or not and check if it is still valid or not. Using react-native-iap package
RNIap.getPurchaseHistory().catch(() => {
console.log("error fetching purchase history")
}).then((res) => {
// here you will get all the purchase history
// filter with the product id for which you want to check
// if record exist that means you have to check its validity
})
checking validity is not straight forward. you need use code given bellow. However, receipt in receiptBody bellow can be any receipt since response does not shows its expiry date. Response has 'latest_receipt_info' that contain all the purchase with expiry date from where you can filter the one you need.
const receiptBody = {
"receipt-data" : receipt,
"password" : "" //password from the App-Specific Shared Secret in appstoreconnect in-app-purchase section for the app
}
await RNIap.validateReceiptIos(receiptBody, true).then((receipt) => {
try {
const renewalHistory = receipt.latest_receipt_info
// it has all purchase with expiry date
// filter for the one you are looking for
} catch (error) {}
})
Receipt can be any receipt in receiptBody above so you can get current receipt which is fast to implement like bellow
await RNIap.getReceiptIOS().then((res) => {
currentReceipt = res
})
You may want to consider using other third party libraries to handle receipt processing that provide clearer direction for how to handle in-app purchase.
One example is NamiML, which has a guide on how to check for active purchases here that has a react native example:
https://docs.namiml.com/docs/react-to-changes-to-entitlements
The code for what you want to check using the NamiML library looks like:
NativeModules.NamiEntitlementManagerBridge.isEntitlementActive("premium_access", (active) => {
if (active) {
// react to entitlement being active
}
});
For react native iap, if the user purchases the app but they already own it, you'll get a purchase error code of "E_ALREADY_OWNED". You can use async storage to store a variable to check if the page has already been purchased; if not, then go to the page and award it.
useEffect(() => {
if (currentPurchaseError) {
if (
currentPurchaseError.code === "E_ALREADY_OWNED" &&
!isPagePurchased
) {
setAndStorePagePurchase(true)
}
}
}, [currentPurchaseError])
See this example for how to implement this into a full simple app.
I've used RevenueCat with relative success recently. They're free if you make less than 10k(?) in a year through the app. With their sdk react-native-purchases I implemented something like this, with the consideration that I only have 1 IAP:
try {
const purchaserInfo = await Purchases.getPurchaserInfo();
if (purchaserInfo && Object.entries(purchaserInfo.entitlements.active).length) {
setActive(true); //There exists an entry for the purchase they made,
//meaning they're an active customer
} else {
setActive(false);
}
const offerings = await Purchases.getOfferings();
if (offerings && offerings.current) {
setPrice(offerings.all.fullVersionOffering.availablePackages[0].product.price_string);
} else {
setPrice('$1.99');
}
} catch (error) {
console.log(error);
}

Managing Google Play subscriptions not working

My app has in-app subscription purchases but I am feeling really confused regarding the whole implementation process.
When the app opens I am calling onPurchasesUpdated(int responseCode, #Nullable List<Purchase> purchases) to check if the user has any active purchases. If the purchase list is null, the app assumes the user has not purchased anything.
Later when user decides to purchase something app calls:
mBillingClient = BillingClient.newBuilder(view.getContext()).setListener(new PurchasesUpdatedListener()
Once the purchase is made again onPurchasesUpdated(int responseCode, #Nullable List<Purchase> purchases) is called, however, once I reopen the app nothing works, its all back to normal (free version) like user purchased nothing.
Also, user purchase data wasn't stored in the cloud (firebase real-time database). Already three users have made their purchase and it's in Three Day Trial period.
Have you looked at the sample on GitHub? When user opens your app you are supposed to call queryPurchases. Here is an example:
fun queryPurchasesAsync() {
Log.d(LOG_TAG, "queryPurchasesAsync called")
val purchasesResult = HashSet<Purchase>()
var result = playStoreBillingClient.queryPurchases(BillingClient.SkuType.INAPP)
Log.d(LOG_TAG, "queryPurchasesAsync INAPP results: ${result?.purchasesList?.size}")
result?.purchasesList?.apply { purchasesResult.addAll(this) }
if (isSubscriptionSupported()) {
result = playStoreBillingClient.queryPurchases(BillingClient.SkuType.SUBS)
result?.purchasesList?.apply { purchasesResult.addAll(this) }
Log.d(LOG_TAG, "queryPurchasesAsync SUBS results: ${result?.purchasesList?.size}")
}
processPurchases(purchasesResult)
}
And you should make this call as soon as you establish connection with the BillingClient service:
/**
* This is the callback for when connection to the Play [BillingClient] has been successfully
* established. It might make sense to get [SkuDetails] and [Purchases][Purchase] at this point.
*/
override fun onBillingSetupFinished(billingResult: BillingResult) {
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
Log.d(LOG_TAG, "onBillingSetupFinished successfully")
querySkuDetailsAsync(BillingClient.SkuType.INAPP, GameSku.INAPP_SKUS)
querySkuDetailsAsync(BillingClient.SkuType.SUBS, GameSku.SUBS_SKUS)
queryPurchasesAsync()
}
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> {
//Some apps may choose to make decisions based on this knowledge.
Log.d(LOG_TAG, billingResult.debugMessage)
}
else -> {
//do nothing. Someone else will connect it through retry policy.
//May choose to send to server though
Log.d(LOG_TAG, billingResult.debugMessage)
}
}
}

Android: Getting user's location using Admob's Consent SDK

I have an issue with AdMob's new 'Consent SDK'; the integration guide says to put the following into onCreate...
public class MainActivity extends Activity {
...
#Override
protected void onCreate(Bundle savedInstanceState) {
...
ConsentInformation consentInformation = ConsentInformation.getInstance(context);
String[] publisherIds = {"pub-xxxxxxxxxxxxxxxx"};
consentInformation.requestConsentInfoUpdate(publisherIds, new ConsentInfoUpdateListener() {
#Override
public void onConsentInfoUpdated(ConsentStatus consentStatus) {
// User's consent status successfully updated.
}
#Override
public void onFailedToUpdateConsentInfo(String errorDescription) {
// User's consent status failed to update.
}
});
...
}
...
}
And then make a call to:
ConsentInformation.getInstance(context).isRequestLocationInEeaOrUnknown()
The problem I'm having is that when the app is first installed/launched, isRequestLocationInEeaOrUnknown() always returns 'false' (I am in the EEA by the way).
If I then exit the app and re-launch it, it returns 'true' - this is correct. If I then go into my device settings and perform a 'clear data' on my app and re-launch it, once again it returns 'false'.
Obviously this is worrying as I am showing my own custom consent dialog to EEA/Swizerland users than I am the ROW. And this needs to happen on first launch.
Interestingly, I tried putting the call to isRequestLocationInEeaOrUnknown() in my AsyncTask' doInBackground method (I kick this ASync off in onCreate) and then it does return 'true' on first-launch as do calls to it made in the ASync's 'onPostExecute' method. It's just that calls made to it in onCreate do not (before or after the Async starts).
I know it's early days, but has anyone stumbled upon similar issues with this?
You must call isRequestLocationInEeaOrUnknown() after onConsentInfoUpdated callback is called.
This value is retrieved asynchronously by requestConsentInfoUpdate(), so it is not correct at first launch, but it is then cached so on second launch you have correct value.
Since i faced the same issue, and the docks are a bit confusing I will try to explain how it works.
add this for testing only remove them in production and make sure you add them before you request consent
ConsentInformation.getInstance(this#MainActivity).addTestDevice("DEVICE_ID") // add your device id for testing
ConsentInformation.getInstance(this#MainActivity).debugGeography = DebugGeography.DEBUG_GEOGRAPHY_EEA // test if user in EEA
ConsentInformation.getInstance(this#MainActivity).consentStatus = ConsentStatus.UNKNOWN // this sets the state to unknown, useful to reset the consent state in between tests careful to remove this if you want to see the flow for a returning user
Request consent status like this:
val consentInformation = ConsentInformation.getInstance(this#MainActivity)
val publisherIds = arrayOf(ADMOB_PUBLISHER_ID)
consentInformation.requestConsentInfoUpdate(publisherIds, object: ConsentInfoUpdateListener {
override fun onFailedToUpdateConsentInfo(reason: String?) {
// consent request failed so probably you sould display non personalized ads
log("MAIN ACTIVITY", "FAILED TO UPDATE CONSENT SHOW NOT PERSONALIZED")
initializeAds(NON_PERSONALIZED)
// YOU COULD RETRY HERE OR IT WILL RETRY ON NEXT SESSION
}
override fun onConsentInfoUpdated(consentStatus: ConsentStatus?) {
when (consentStatus) {
ConsentStatus.PERSONALIZED -> {
log("MAIN ACTIVITY", "USER OPTED FOR PERSONALIZED")
// USER ALREADY GAVE HIS CONSENT FOR YOUR PUBLISHER ID SO YOU CAN DISPLAY PERSONALIZED ADS
initializeAds(PERSONALIZED)
}
ConsentStatus.NON_PERSONALIZED -> {
log("MAIN ACTIVITY", "USER OPTED FOR NON_PERSONALIZED")
// USER OPTED FOR NON_PERSONALIZED ADS SO INCLUDE THAT IN YOUR ADD REQUEST
initializeAds(NON_PERSONALIZED)
}
ConsentStatus.UNKNOWN -> {
log("MAIN ACTIVITY", "USER CONSENT STATUS IS UNKNOWN ")
// USER WAS NEVER PROMPTED TO GIVE HIS CONSENT (DEFAULT STATE FOR ALL USERS)
if (consentInformation.isRequestLocationInEeaOrUnknown) {
log("MAIN ACTIVITY", "USER IS IN EEA REQUEST CONSENT ")
// USER IS IN THE EEA AREA SO WE NEED TO REQUEST HIS CONSENT (WE SHOW THE PROMPT HERE) YOU SHOULD UPDATE CONSENT WITH HIS OPTION SO THIS FUNCTION WILL NEVER GET CALLED AGAIN
requestConsentFromUser()
} else {
log("MAIN ACTIVITY", "USER NOT IN EEA INITIALIZE ADS ")
// USER IS NOT IN EEA SO WE ARE NOT REQUIRED TO REQUEST CONSENT (YOU COULD STILL REQUEST IT IF YOU LIKE)
initializeAds(PERSONALIZED)
}
}
}
}
})
I face the same, some countries are correct, some aren't.
I tried "https://adservice.google.com/getconfig/pubvendors" too

Google play services doesnt displays leaderboard after Auth

Its a very weird have invested a lot of time in this. So asking here.
I am using this plugin and followed the steps https://github.com/playgameservices/play-games-plugin-for-unity
I just want to use Google play services in release mode for leaderboard only (i am using in alpha launch)
Here is my code for Auth in GPlay services :
void Awake(){
PlayerPrefs.SetInt("GameOverCount",0);
#if UNITY_ANDROID
Authenticate ();
#endif}
I have configured Gplay setup by providing 10+ digit Client ID
I have generated OAuth Client ID from Google APis using SHA1 of release.keystore that i am using.
In Gplay service in DEveloper console : I have published the linked apps without any errors
public void Authenticate() {
if (Authenticated || mAuthenticating) {
Debug.LogWarning("Ignoring repeated call to Authenticate().");
return;
}
PlayGamesClientConfiguration config = new PlayGamesClientConfiguration.Builder()
.EnableSavedGames()
.Build();
PlayGamesPlatform.InitializeInstance(config);
// Activate the Play Games platform. This will make it the default
// implementation of Social.Active
PlayGamesPlatform.Activate();
// Set the default leaderboard for the leaderboards UI
((PlayGamesPlatform) Social.Active).SetDefaultLeaderboardForUI(leaderboardID_android);
// Sign in to Google Play Games
mAuthenticating = true;
Social.localUser.Authenticate((bool success) => {
mAuthenticating = false;
if (success) {
UnityAnalytics.CustomEvent("Auth Completed", new Dictionary<string, object>
{
{ "isAuthDone", true }
});
if(showScores){
Social.Active.ShowLeaderboardUI();
Debug.Log("should show leaderborad");
}
// if we signed in successfully, load data from cloud
Debug.Log("Login successful!");
} else {
// no need to show error message (error messages are shown automatically
// by plugin)
Debug.LogWarning("Failed to sign in with Google Play Games.");
UnityAnalytics.CustomEvent("Auth Failed", new Dictionary<string, object>
{
{ "isAuthDone", false }
});
}
});
}
#if UNITY_ANDROID
if (Authenticated) {
showScores = false;
((PlayGamesPlatform)Social.Active).ShowLeaderboardUI(leaderboardID_android);
Social.ShowLeaderboardUI();
Debug.Log("should show leaderborad");
UnityAnalytics.CustomEvent("Authenticated and show android leaderboard", new Dictionary<string, object>
{
{ "isShowing", true }
});
}else{
showScores = true;
Authenticate();
}
#endif
Next, i release the alpha and check on android devices - i get a screen saying connecting to google play games...connects processes and then goes off the screen.
It seems like it authenticates and goes off. But doesn't displays leaderboard for some reason. I am not able to understand why its happening and what's missing.
In my unity analytics, only once scores aren't loaded and i played it 20-30 times :
In my Developer console, Number of scores is empty :
Can anybody pls help in this, i can provide more details to people who can help / want to help....Thanks...

Categories

Resources