Flutter Android revenuecat subscription status is active even after expiration - android

I am using Revenuecat (purchases_flutter: ^3.7.0) plugin in my Flutter Android app. However I noticed, even if the (test) subscription has been ended the code shows it is still active.
I am using the following method to determine whether the subscription is active or not.
static isUserSubscribed() async {
bool _isSubscribed = false;
await Purchases.setDebugLogsEnabled(false);
await Purchases.setup(revenueCatId);
if (userId != null) {
await Purchases.setup(revenueCatId, appUserId: userId);
PurchaserInfo purchaserInfo = await Purchases.getPurchaserInfo();
if (purchaserInfo.entitlements.all["Ads Free"] != null &&
purchaserInfo.entitlements.all["Ads Free"].isActive != null &&
purchaserInfo.entitlements.all["Ads Free"].isActive) {
// it comes here even though the subscription has been expired
_isSubscribed = true;
}
return _isSubscribed;
}
I noticed, when I do test purchase and even after the subscription has been ended the above method returns true always. I even restarted the app and logged out and even clear the memory too.
If I go to revenucat.com and remove the user by searching the email address then it start returning false. My test subscription was expired in the morning around 9:00 AM but in the evening also it showing active in the app.
Any help will be appreciated

Related

Google Play In-app messaging not displaying payment declined message

I'm trying to implement In-app messaging to display a snackbar if a subscription has had it's payment declined.
Following the documentation here and adding billingClient.showInAppMessages doesn't seem to work. I subscribe using the Test card, always approves and change it to Test card, always declines and wait for the payment to be put in grace period, but the snackbar from the documentation does not show up even after restarting the application.
Expected result after payment has been declined and app was restarted:
In-app messaging works as I can send messages via firebase, but I am unsure if I'm missing something obvious here?
Implementation:
(This is called on app start)
// onCreate
billingClient = createBillingClient()
setupInAppMessaging(activity)
if (!billingClient.isReady) {
logD { "BillingClient: Start connection..." }
billingClient.startConnection(this)
}
fun createBillingClient() = BillingClient.newBuilder(context)
.setListener(this)
.enablePendingPurchases()
.build()
fun setupInAppMessaging(activity: Activity) {
val inAppMessageParams = InAppMessageParams.newBuilder()
.addInAppMessageCategoryToShow(InAppMessageParams.InAppMessageCategoryId.TRANSACTIONAL)
.build()
billingClient.showInAppMessages(activity, inAppMessageParams) { inAppMessageResult ->
if (inAppMessageResult.responseCode == InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED) {
// The flow has finished and there is no action needed from developers.
logD { "SUBTEST: NO_ACTION_NEEDED"}
} else if (inAppMessageResult.responseCode == InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED) {
logD { "SUBTEST: SUBSCRIPTION_STATUS_UPDATED"}
// The subscription status changed. For example, a subscription
// has been recovered from a suspend state. Developers should
// expect the purchase token to be returned with this response
// code and use the purchase token with the Google Play
// Developer API.
}
}
}
Has it worked at all? I believe these messages are only shown once per day so if you already saw it once, you will have to wait another 24 hours to see it again if your payment method is still failing.

Flutter App crashes when taking pictures erratically

I'm working on an app that includes taking pictures and sending them to the backend. Nothing crazy, and everything seems to be good. Since we need to send information back to the server, I need implemented something such that the app verifies the connection token's validity every time the App's state goes from Paused or Exited to Resumed.
That being said, a friend of mine was testing the app and apparently taking pictures from his Samsung S20 FE (android 12) can crash the app. It happens at seemingly random times, so I've plugged his phone into Android Studio and go this error message:
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: Error During Communication: Error occured while Communication with Server with StatusCode: 404
That's really weird because we don't try to send the pictures until a button is pressed and this happens right as the picture is taken. So I thought it might be my code that checks for lifecycle changes, since it does call the backend to check on the auth token, but it's surrounded with a .then and .onError which should catch the network error before it's propagated to the app.
It also bugs me that I've never been able to reproduce it with my OnePlus 6 (Android 11) or using a simulator. I'll be joining some code below to illustrate my functionalities. Any idea why I'm having this problem and how I can fix it?
Thanks in advance !
App lifecycle function
#override
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
print("State changed to: $state");
switch(state){
case AppLifecycleState.resumed:
final LoginManager loginManager = locator<LoginManager>();
final userToken = await loginManager.getUserTokenFromStorage();
if (userToken != null) {
final Future<LoginResponse?> loginResponse = locator<LoginManager>().verifyToken(refreshToken);
loginResponse.then((value) {
if(value == null) {
_showTimeoutDialog(navigatorKey.currentContext!);
}
}).onError((error, stackTrace) {
_showTimeoutDialog(navigatorKey.currentContext!);
});
}
break;
case AppLifecycleState.inactive:
// TODO: Handle this case.
break;
case AppLifecycleState.paused:
// TODO: Handle this case.
break;
case AppLifecycleState.detached:
// TODO: Handle this case.
break;
}
}
Note that showTimeoutDialog shows a dialog and properly disconnects the user, no information is sent whatsoever.
Taking a picture function
Future<File?> _getPhotoFromCamera(bool isSelfie) async {
return ImagePicker()
.pickImage(
source: ImageSource.camera,
preferredCameraDevice:
isSelfie ? CameraDevice.front : CameraDevice.rear,
)
.then((value) => value != null ? File(value.path) : null);
}
Uses the flutter image_picker package

Flutter connectivity false callback

Im using connectivity package to track users connection changes. The idea is to pop up a warning page for connection loss when the ConnectivityResult is none (aka wifi and mobile is disconnected). But instead i get these results :
If the wifi is connected and you disconnect it, 50% of the time the warning pops up.
If you are on mobile and turn it off, the connectivity returns that user is still on ConnectivityResult.mobile not ConnectivityResult.none.
Tried to make a doublecheck with pinging google, but even that doesnt work as smooth as i expected it to be.
My code :
I have created seperated file with functions :
void trackNetworkStatus(BuildContext ctx) {
//check in case state is just opened
pingGoogle(ctx);
//add network listener
Connectivity().onConnectivityChanged.listen((ConnectivityResult result) {
print("Network changed to $result");
//if user lost connection
if (result == ConnectivityResult.none) {
openNoInternetScreen(ctx);
}else{
//if user has connection, doublecheck
//mobile network is tricky on android
pingGoogle(ctx);
}
});
}
Future<void> pingGoogle(BuildContext ctx) async {
try {
//ping internet page
final result = await InternetAddress.lookup('google.com');
//if ping is successful
if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) {
print('connected');
}else{
print('not connected');
openNoInternetScreen(ctx);
}
} on SocketException catch (_) {
print('not connected');
openNoInternetScreen(ctx);
}
}
void openNoInternetScreen(BuildContext ctx) {
Navigator.push(
ctx,
MaterialPageRoute(builder: (context) => noInternetPage()),
);
}
because i am calling them on every apps init like this :
#override
void initState() {
super.initState();
//listen to netwoek changes
trackNetworkStatus(context);
}
which leads to problem that sometimes warning page pops up twice because, as i believe, the previous listener has not been stopped, but i can figure out how to fix it. The question is why connectivity package returns false callback on mobile. Ive tested on virtual Android API 23 and Samsung S9+, both share same results.
I gave my app to couple early testers and everything turned out just fine for them, even if some of them own Samsung devices too. Looks like there is no problem at all with the connectivity package, some of Samsung phones just act weird as they shouldnt. Looks like there will always be a black sheep in the whole community, sadly thats me and im the developer with this buggy device.
Havent found the fix for my device, but looks like the package is safe to go for the most of the devices in market.
The phone model, that is acting strange : SM-G965F.

Checking if in-app subscriptions is active in my serverless android app

I have the serverless android app with simple functional: if user has some in-app subscription (auto renewable), then he can use functional in app, otherwise there is no. I know, how to make functional with obtaining subscriptions info (price, title etc) and calling payment. But I can not check if current user has active (not cancelled) subscriptions. I read so many information on many sites and tutorials, and there was written that I must use google API in my server. But I do not have my own server.
I used two different libraries for in-app subscriptions:
'com.anjlab.android.iab.v3:library:1.0.44'
and
'com.android.billingclient:billing:1.1'
but no one helped me for checking if user has active subscriptions. So, how to make this task? Help me please, maybe I missed some information...
Edit: Anjlab library hasn't been updated in the longest time ever. Kindly use Google's own billing library, this step-by-step process should help you easily integrate it into your app - Google In App Billing library
Using the anjlab in-app-billing library I was also facing the similar. This is what I did to get around it.
Invoke the method billingProcessor.loadOwnedPurchasesFromGoogle(); Then check the value of transactionDetails, if the TransactionDetails object return null it means, that the user did not subscribe or cancelled their subscription, otherwise they are still subscribed.
void checkIfUserIsSusbcribed(){
Boolean purchaseResult = billingProcessor.loadOwnedPurchasesFromGoogle();
if(purchaseResult){
TransactionDetails subscriptionTransactionDetails = billingProcessor.getSubscriptionTransactionDetails(YOUR_SUBSCRIPTION_ID);
if(subscriptionTransactionDetails!=null)
//User is still subscribed
else
//Not subscribed
}
}
Also, point to note is that the TransactionDetails object will only return null after the period of the subscription has expired.
Have you try to call bp.loadOwnedPurchasesFromGoogle(); ?
Edit
So try this :
Purchase purchase = inventory.getPurchase(product);
Log.d(TAG, "Purchase state: " + purchase.getPurchaseState());
// 0 (purchased), 1 (canceled), or 2 (refunded).
if (purchase.getPurchaseState() == 0
|| purchase.getPurchaseState() == 2) {
showPremiumVersion();
} else {
showFreeVersion();
}
Or this solution :
bp.isPurchased("yourSKU")
The isPurchsed method can't catch history of error's purchase / canceled Purchase/ Retrived Purchase.
Use this and don't forget to add INTERNET permission in your manifest.
TransactionDetails transactionDetails = billingProcessor.getPurchaseTransactionDetails("productId");
if (transactionDetails != null) {
//Already purchased
//You may save this as boolean in the SharedPreferences.
}

AzureAD for Android throws ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED

I have an app in which user authentificates in Office365 with AzureAD library for Android.
It works well, users can authentificate and work with the app. Unfortunately, after a while they start hitthing AuthenticationException with ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED as an error code.
I checked the source code of AzurelAD. The only place, which is throughing this issue is acquireTokenAfterValidation() method:
private AuthenticationResult acquireTokenAfterValidation(CallbackHandler callbackHandle,
final IWindowComponent activity, final boolean useDialog,
final AuthenticationRequest request) {
Logger.v(TAG, "Token request started");
// BROKER flow intercepts here
// cache and refresh call happens through the authenticator service
if (mBrokerProxy.canSwitchToBroker()
&& mBrokerProxy.verifyUser(request.getLoginHint(),
request.getUserId())) {
.......
Logger.v(TAG, "Token is not returned from backgroud call");
if (!request.isSilent() && callbackHandle.callback != null && activity != null) {
....
} else {
// User does not want to launch activity
String msg = "Prompt is not allowed and failed to get token:";
Logger.e(TAG, msg, "", ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED);
callbackHandle.onError(new AuthenticationException(
ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED, msg));
}
// It will start activity if callback is provided. Return null here.
return null;
} else {
return localFlow(callbackHandle, activity, useDialog, request);
}
}
My source code:
authenticator.getAccessTokenSilentSync(getMailService());
public class Authenticator {
..............
public String getAccessTokenSilentSync(ServiceInfo serviceInfo) {
throwIfNotInitialized();
return getAuthenticationResultSilentSync(serviceInfo).getAccessToken();
}
private AuthenticationResult getAuthenticationResultSilentSync(ServiceInfo serviceInfo) {
try {
return authenticationContext.acquireTokenSilentSync(
serviceInfo.ServiceResourceId,
Client.ID,
userIdentity.getAdUserId());
} catch (AuthenticationException ex) {
// HERE THE EXCEPTION IS HANDLED.
}
}
..............
}
Stacktrace I'm getting:
<package name>.data_access.error_handler.AuthenticationExceptionWithServiceInfo: Refresh token is failed and prompt is not allowed
at com.microsoft.aad.adal.AuthenticationContext.localFlow(AuthenticationContext.java:1294)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenAfterValidation(AuthenticationContext.java:1229)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenLocalCall(AuthenticationContext.java:1123)
at com.microsoft.aad.adal.AuthenticationContext.refreshToken(AuthenticationContext.java:1609)
at com.microsoft.aad.adal.AuthenticationContext.localFlow(AuthenticationContext.java:1261)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenAfterValidation(AuthenticationContext.java:1229)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenLocalCall(AuthenticationContext.java:1123)
at com.microsoft.aad.adal.AuthenticationContext.refreshToken(AuthenticationContext.java:1609)
at com.microsoft.aad.adal.AuthenticationContext.localFlow(AuthenticationContext.java:1261)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenAfterValidation(AuthenticationContext.java:1229)
at com.microsoft.aad.adal.AuthenticationContext.acquireTokenLocalCall(AuthenticationContext.java:1123)
at com.microsoft.aad.adal.AuthenticationContext.access$600(AuthenticationContext.java:58)
at com.microsoft.aad.adal.AuthenticationContext$4.call(AuthenticationContext.java:1072)
at com.microsoft.aad.adal.AuthenticationContext$4.call(AuthenticationContext.java:1067)
at java.util.concurrent.FutureTask.run(FutureTask.java:237)
Version of AzureAD library I'm using: 1.1.7 (to prevent blaming too old version - I've checked the changelist since from 1.1.7 to 1.1.11 and haven't found anything related to question)
Problem: Right now, I'm treating this error, as a signal to through the user to the login screen. In my opinion, it leads to a poor experience for the user. The fact that it happens very often and affects many users make it even worse.
Question: Is there anything I can do different to avoid this AuthenticationException or workaround it somehow (i.e. avoid user enters credentials once again).
Have you verified that AuthenticationContext.acquireTokenSilentSync() is truly the method that you wish to invoke?
The docs indicate that this method will explicitly not show a prompt. From the docs:
This is sync function. It will first look at the cache and automatically checks for the token expiration. Additionally, if no suitable access token is found in the cache, but refresh token is available, the function will use the refresh token automatically. This method will not show UI for the user. If prompt is needed, the method will return an exception.
The refresh token you are issued should last two weeks per this AAD book. After the refresh token expires users are expected to reauthenticate. Can you inspect net traffic with Fiddler or Charles and inspect the expiry of the tokens? If you can verify that the tokens are failing to refresh before their expiry it may indicate a bug in the AD library.
To clarify the difference in methods on AuthenticationContext - there are two categories of methods: "silent" methods (which will not present a dialog to user in the event that they need to reauthenticate), and non-silent. Non-silent methods will, in the event of requiring reauthentication (or consent) from the user, start a new Activity containing the AAD login. At that point the authentication flow is restarted.
Additionally, if you make changes to your application's registration in Azure such as adding new permission scopes your users will be required to re-grant consent for the application to continue to handle their data.
This is because you need to refresh your token and implement this in your code so the user won't be prompt to login every time the access token is expired. please check out how to implement refresh token here:
https://msdn.microsoft.com/en-us/library/azure/dn645538.aspx
Hope this helps.

Categories

Resources