Question

Can I check if user is subscribed on flutter web app?

  • 15 April 2023
  • 18 replies
  • 500 views

Badge +4

I am looking to be able to show unlocked features in a web based version of my app. If I make it so users can subscribe in either the iPhone or Android store but still access un-locked content on the web or desktop versions? 

 


18 replies

Userlevel 3
Badge +6

Hey @Mike Jones ,

 

If your users have the same user ID, then the subscriptions will be shared across multiple platforms and all you will need to do is call getCustomerInfo() to access if they have a subscription or not. 

 

More information on checking if they have an active subscription can be found here: https://www.revenuecat.com/docs/customer-info#get-user-information

 

Badge +4

@Michael Fogel but this plugin only shows it works on Android and Apple: https://pub.dev/packages/purchases_flutter

So not sure web and PC work with flutter? 

Badge +6

 @Mike Jones: Below is a code snippet that I am using on web, which simply goes via the API rather than the Flutter SDK.

  Future<bool> _getSubscriptionStatusWeb(User user) async {
bool hasActiveSubscription = false;
try {
Uri uri = Uri.https('api.revenuecat.com', '/v1/subscribers/${user.id!}');
await http.get(uri, headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $revenueCatApiKey',
}).then((response) {
if (response.statusCode == 200) {
Map<String, dynamic> customerInfo = json.decode(response.body);
Map<String, dynamic> entitlements =
customerInfo['subscriber']['entitlements'];
if (entitlements.isNotEmpty) {
List<DateTime?> expiryDates = entitlements.values
.map((entitlement) =>
DateTime.tryParse(entitlement['expires_date']))
.toList();
hasActiveSubscription = expiryDates.firstWhereOrNull(
(element) => element!.isAfter(DateTime.now())) !=
null;
}
}
});
} catch (e) {
debugPrint("Unable to retrieve subscriber status from Revenuecat");
}
return hasActiveSubscription;
}
Badge +4

@Matt thats awesome! But unfortunately my web is flutter web, so all the code is written in dart…. not sure that will work for m y situation. 

Badge +6

@Mike Jones - I’m not sure I understand the issue. The code snippet is written in dart and simply queries a web api. This will work regardless of any platform, as long as you have an internet connection.

Badge +4

@Matt thats awesome! But unfortunately my web is flutter web, so all the code is written in dart…. not sure that will work for m y situation. 

yup I was just tired… lol

I will give this a try, thanks!

Badge +4

@Matt I went to implement this today and got these errors:

Undefined name 'http'.

and

The method 'firstWhereOrNull' isn't defined for the type 'List'.
Try correcting the name to the name of an existing method, or defining a method named 'firstWhereOrNull'.

 

Assuming $revenueCatApiKey I just replace with my api key? And User user I can change to a string to my user ids? 

Badge +6

I'm sorry, but may I suggest you rethink your approach to dealing with this? It appears you want to go the way of least resistance and have someone work it out for you, rather than learning how to build a solution for yourself. It took me about 5 seconds to find the answers to those questions on http and firstWhereOrNull. With respect to the api key I would suggest to review the http package and the fundamental principles of a web api.

Badge +2

Don’t copy this guys dart code and use it in your public app - you need an API secret to fetch this data and should never be exposed. 

Place this function on a backend server, secure your api key and return the result from your own endpoint that requires authentication.

Badge +6

Interesting - it appears there may have been a change since the original reply with the code snippet as @Thruday seems to be right. At the time I proposed this solution it worked with a legacy public key. This no longer seems to be the case with either legay or current public keys.

As such, please review for yourself carefully and reconsider the above solution.

Edit: The original solution is still valid and still works with (legacy) public API keys. Thus it is safe to use.

Badge +2

No it is not safe to use, you need a secret API key to access these records and, if using the V2 API you need to set explicit permissions.which I wouldn’t just be publishing on the app store. Its not that hard to reverse engineer code and extract information.

If you care about your businesses safety and customers privacy - just setup the backend route as to avoid anyone accessing random user data.

Source: CTO with 13 years experience programming complex systems.

 

What concerns me is that no one from RC picked up on this (despite documentation) and has left vulnerable code live for copy and paste months/
 

Badge +2

Here is some code I was messing with last night that ensures no credential leaks and is capable of keeping a web user and a mobile user in sync using a single Provider.

Having spent an entire day breaking down the package, api end points - I really don’t see why RC cannot add a customer back end route config that allows our servers to hook into the flutter plugin. I mean, the plugin uses API end points, why cant we just provide a route that simply forwards the API result from our own server and then have a cross platform plugin that works on web too?

Yes, we have no real stripe integration but again, its something stripe connect api can fix by generating payment links.

What is most frustrating is you guys also say that you use the API directly with the SDK however, have you guys tried making a customer call and using the .toJson functions provided for customerInfo or entitlements? It doesnt work - if it worked, all this would be as simple as contacting the backend and then using CustomerInfo.toJson on the result and then propagating that through the system. 

Things shouldn’t be this hard or convoluted when the functionality clearly exists in the flutter sdk. What it feels like right now is bad programming.
 

class AppSubcriberRecord {

 

  // product they are subscribed too

  String? productId;

 

  // date is renews or expires

  DateTime? expiresAt;

 

  // unsubscribe detected at

  DateTime? unsubscribedAt;

 

  // the url to manage

  String? managementUrl;

 

  // is the current user subscribed

  bool get isSubsribed => null != expiresAt && expiresAt!.isAfter(DateTime.now());

 

  // will the user renew

  bool get willRenew => isSubsribed && null == unsubscribedAt ? true : false;

 

  AppSubcriberRecord({

    this.productId,

    this.expiresAt,

    this.unsubscribedAt,

    this.managementUrl,

  });

 

}



 

  // custom subscription tracker - defined above

  AppSubcriberRecord? subscription;


  // contact backend, use the auth token for the user id and forward the result to the app

  Map<String, dynamic> response =

    await Api.get('/purchases/revenuecat/subscription-status', AuthedUser.userAuthToken);

 

  Map<String, dynamic>? activeEntitlement;

  Map<String, dynamic> customerInfo = response;

  Map<String, dynamic> entitlements = customerInfo['subscriber']['entitlements'];

 

  if (entitlements.isNotEmpty) {

 

    // grab subscription to find the unsubscribed date

    Map<String, dynamic> subscriptions = customerInfo['subscriber']['subscriptions'];

   

    // sort the entitlements and fetch the associations subscriptions

    var mergedEntitlements = entitlements.values.map((entitlement){

      // grab the id of the product - we need the unsubscribed at

      var productId = entitlement['product_identifier'];

      // fetch that product

      var product = subscriptions[productId];

 

      // combine entitlement + product

      return {

        'product_id': productId,

        'entitlement': entitlement,

        'product': product,

        'expires_date': DateTime.tryParse(entitlement['expires_date'])

      };

    }).toList();

 

    // find out if we have an active entitlement

    activeEntitlement = mergedEntitlements.firstWhereOrNull((entitlement)

      => entitlement['expires_date']?.isAfter(DateTime.now()));

 

    // init our custom tracker

    subscription = AppSubcriberRecord(

      productId: activeEntitlement?['product_id'],

      expiresAt: activeEntitlement?['expires_date'],

      unsubscribedAt: activeEntitlement?['product']['unsubscribed_at'],

      managementUrl: activeEntitlement?['management_url']

    );

 

    print(['subscribed', subscription.isSubsribed]);

    print(['will review', subscription.willRenew]);

    print(['renews / expires', subscription.expiresAt]);  

    print(['manage url', subscription.managementUrl ]);  

  }

 

} catch(e, stack){

  print(['error', e, stack]);

}

Badge +6

No it is not safe to use, you need a secret API key to access these records [...]

 

The RC client side SDKs use public API keys, so how is the code snippet that works with a public key really any different? If you are saying it is unsafe, can you explain why it is unsafe and where there is a key difference to using the RC client side SDKs in the first place when it comes to security? Either option can be reverse engineered.

 

Source: CTO with 13 years experience programming complex systems.

 

Good for you, but I’m not sure how this contributes to finding the correct answer.

Badge +2

Their documentation explains why secret keys should never be exposed on the public side and I clearly explained why above with the risk of reverse engineering and data leaks. Their API documentation states you need a secret keys to make these calls - secret keys are supposed to be controlled and kept - secret.

You can do your own research on private / public keys and deduce the same. Its basic backend programming and security.

You cannot call configure on web first off and, if you follow their examples, you’re using a store config initiated in the main function - this means, you specified a platform specific public api key.

For reference, their recommended usage is here, only one platform specific public key is available at a time, which, would make sense considering we can only call configure one time.: https://github.com/RevenueCat/purchases-flutter/blob/main/revenuecat_examples/MagicWeather/lib/main.dart

 

For references, we have 3 providers;
 

 if(!kIsWeb){

    if (Platform.isIOS || Platform.isMacOS) {

      StoreConfig(

        store: Store.appStore,

        apiKey: 'appl_',

      );

    }

    else if(Platform.isAndroid){

      StoreConfig(

        store: Store.playStore,

        apiKey: 'goog_',

      );

    } else {

     

    }

  } else {

    StoreConfig(

      store: Store.stripe,

      apiKey: 'strp_',

    );

  }


Each platform has its own API key which can be found under your projects and selecting the platform - these are public keys and are intended to give restricted access.

If someone subscribes on Android and then we try to fetch with a stripe public key, I believe you will just face problems. So again - you are back to a secret key and a backend solution for web. What I see is that a public key gives access to a subset of data related to that specific platform which, now I type this out, explains why we need to have our own web implementation. 

If you are handling web subscriptions (which this script is intending to do) then you should be using a backend server anyway to handle a plethora of other functionality. It does not make sense either to have fragmented code with it spread over several places. I could go on, but I wont.

To reiterate; If you follow revenue cats documentation and use private / secret key to fetch your data for web; DO NOT use Matt’s snippet in your frontend code - it is a security issue. Conduct every call via your own backend - if, however, you use flutter only for web and only use stripe then feel free to use the public key however, I cant see why someone would build a web only app on Flutter.

It would be great to get one of the RC devs in here to confirm the above and clarify this. If 9 months has passed and people used their secret keys in public, there is potentially a huge problem in deployment now for some people.

Badge +6

Appreciate the persistence and explanation, which certainly helps to prompt people to learn about public/private keys.

However, the single reason we cannot agree is simply because you talk about the secret key, while I am talking about the public key, and you assume the secret key is needed - which is not the case.

If the secret key would be needed, you would obviously be right, no debate about that. But you simply only need the public key for getting the subscriber status. Run it via any REST API tester and you will see. You can use any public key - legacy public key, the goog_, appl_ or strp_ one - it does not matter.

Finally, please see initialization reference below from the docs:

Make sure you configure Purchases with your public SDK key only. This API key can be found in the API Keys Project settings page. You can read more about the different API keys available in our Authentication guide.

Source: https://www.revenuecat.com/docs/getting-started/quickstart#%EF%B8%8F-initialize-and-configure-the-sdk

So: Public key needed only - plenty of evidence for that.

I’m sorry, there is no problem here, as much as you argue there is.

If 9 months has passed and people used their secret keys in public, there is potentially a huge problem in deployment now for some people.

Using the code snippet provided with a secret key is a huge problem, agreed. Therefore, appreciate you picking this up so that there is, in the end, clarity for people: Use the public key. Period.

Badge +2

You cannot use a stripe public key to fetch data about an android or apple subscription - this defeats the object of having platform specfic public keys.

So you have failed to recognize a problem in the subscription flow - if a user subscribes on Apple and then comes to your web app using a stripe only public key - you wont be getting any meaningful data.

So again, you need a backend solution on web, to use the private key to use get customer to then get access to the entire dataset for that specific identified user instead of just the data for their ‘stripe subscriptions’ - this is clearly defined in their documentation, and, for good reason.

If you’re using revenue cat for only managing stripe subscriptions and using flutter only for web, then sure, use matts code with a public key, it will work perfectly but, if you have a multi-platform app, you need a backend, secret key solution to access the users full dataset.

Its fair to say, anyone on flutter will be using two different platforms - Android and Apple. If you’re using Web then that is three platforms with three separate api keys all designed to fetch data specific to each platform so no, you cannot simply just use a public key and expect to fetch an authenticated users data on web, it wont work, the public key will only return stripe transactions.

Can we please get RC Tech in here to confirm this please.


 

Badge +6

So you have failed to recognize a problem in the subscription flow - if a user subscribes on Apple and then comes to your web app using a stripe only public key - you wont be getting any meaningful data.

This is simply incorrect - please try for yourself.

Can we please get RC Tech in here to confirm this please.

Practice shows public keys can be used cross-platform.

@Michael Fogel - You you or someone else maybe answer the following?

  • Is this by design and, if yes, what is the logic behind this design choice?
  • What is the progress on a web SDK and is there any arrival date?
  • Can you recommend a good practice without web SDK and additional backend?
  • Can you update the docs and/or the original answer with that good practice recommendation and put an additional warning in above that a secret key should not be used in this case?
Badge +2

Ok, then this raises the next issue; 

Why do we configure separate public keys for different platforms if they all have access to the same data? It seems like a unnecessary complication to me, or am I missing something?

Ultimately thought Matt, we need RevenueCat to implement the web side effectively, the tools are there, the framework is there in the code but, when you try and interchange api calls with SDK functions to parse the data - it simply doesn’t work.

From my perspective, if we can get CustomerInfo.fromJson to actually work with an API call we make ourselves on web, I cannot see why we cannot initiate RevenueCat on web with the same functions we use on web, unless, I am missing something here?

 

Example;

 

// fetch from the backend or with public key on the frontend 

Map<String, dynamic> response =

    await Api.get('/purchases/revenuecat/subscription-status', AuthedUser.userAuthToken);

  Map<String, dynamic>? activeEntitlement;

 //

  Map<String, dynamic> customerInfo = response;

CustomerInfo customerInfo = CustomerInfo.fromJson(response)

 

Why doesn’t this work? because it seems logical that it should if the SDK if also using the API - instead it just throws an error that it expects a Map<String, dynamic> but got null..


All this aside, the web side is convoluted and there has been requests open for years now on GitHub with almost no action. If there is one thing I have learned;

If it feels complicated, you’re likely doing it wrong and RevenueCat web + stripe feels like exactly that.

I feel like it makes more sense to just drop the SDK, roll my own end points and get consistent control over all platforms rather than needing a bunch of platform detection logic that just bulks out the code and makes it all complicated.



 

Reply