-
Notifications
You must be signed in to change notification settings - Fork 9
Subscriptions,Plans and Features Management
A subscription means a payment agreement, that a customer will subscribe to as a part of the app installation process. It is a monthly recurring payment that the subscribed customer will be charged by Shopify every 30 days. For more information on recurring charge please refer to Shopify API documentation API
If you want to make sure that only subscribed users should be able to access a certain area/feature
of the app then you have two options
1. Extend the ABaseSubscriberController controller. (Recommended)
2. Use RequireSubscription filter.
ABaseSubscriberController internally implements RequireSubscription filter.
Plan: A Plan is a subscription level. You can also refer to it as subscription plan. A higher level subscription plan is more expansive to subscribe and provides more functionalities to the customer.
Feature: App features is a list of functionalities that are associated with a plan/subscription. Features describe and define the capability of a plan.
Example: Lets say you build an app that can create PDF catalogs using the storefront products. You decided to create three levels of plans/subscriptions. They are priced differently and have different functionalities.
Basic Plan ($5.99 / month)
1.1 Add products to the catalog using product custom collections only
1.2 Max 200 products per catalog
Standard Plan ($7.99 / month)
2.1 Products can be added from custom and smart collections
2.2 Max 400 products per catalog
Pro Plan ($10.99 / month)
3.1 Products can be added from custom/smart collections, vendor types and product types
3.2 Max 800 products per catalog
3.3 Catalogs can be shared in Facebook
In this above example, we have three Plans and each of them has different capabilities/features.
As of now there is no administration screen to manage 'Subscription Plans' and associated 'Features'. However an initial list of plans (with features) can be listed in the appsettings.json file and it will be picked up during the app start time and inserted in to the database.
"PlansSeed": [
{
"Name": "Pro", // name of the plan
"TrialDays": 5, // free days
"IsTest": true, // credit card wont be chared in test mode
"DisplayOrder": 1, // display sequence
"Price": 10.99, //usd
"Description": "Pro plan",
"Footer": "Plan footer text",
"Active": true, // show this plan
"IsDev": false, // if true, only developer will see this plan
"IsPopular": true, // if you want to mark a plan as featured plan
"PlanDefinitions": // define the list of features here
[
{
"OptionName": "MaxProducts",
"OptionValue": "800",
"Description": "800 products per catalog"
},
{
"OptionName": "CanShareInFb",
"OptionValue": "yes",
"Description": "Allows pdf catalogs to be shared on facebook"
}
]
}
]
Please refer to the Settings wiki page for details.
But what if you want to delete/modify or even add a new Plan and Features after the app is deployed. As I said before there is no administration screen for that; you will have to go to the database and add them manually.
There are two tables you have to manually enter the data in.
Plans (where you store the plan descriptions)
PlanDefinitions (where you store plan specific features)
There are two ways for doing that
1. Use the helpers ( IPlansReader and IUserCaching )
Using these two helpers you can check if a user has a specific plan, and then check if a plan has the desired option/feature and value. These helpers are already available if you extend the ABaseSubscriberController or any built in controllers that inherits from the ABaseSubscriberController controller.
For example, suppose you want to check inside the RenderPdf() method if the user has 'Pro Plan' and you want to see if what's the max number of products the plan supports to be put into the pdf catalog it is going to render.
Inside the same controller, in another method called CanShareInFb() you want to check if the users's subscribed plan allows to publish the rendered catalog in Facebook.
//Extend ABaseSubscriberController or any child class of it
public class MyCatalogAppController : ABaseSubscriberController
{
//Implement constructor
public MyCatalogAppController(IPlansReader plansReader,
IUserCaching cachedUser,
IConfiguration config,
IDbSettingsReader settings,
ILogger logger) :
base(plansReader, cachedUser, config, settings, logger){}
// Need to check user plan and features for this method
public async Task<IActionResult> RenderPdf()
{
//get user and plan id using this.AppUserCache (IUserCaching)
var currentUser = await this.AppUserCache.GetLoggedOnUser();
var planId = currentUser.PlanId.Value;
// get plan requirement using this.Plans (IPlansReader)
if (this.Plans[planId].Name.Equals("Pro"))
{
var maxItemsCanRender = this.Plans[planId, "MaxProducts"];
}
//....do something with the extracted information
}
// Before sharing to fb lets check if plan allows that feature
public async Task<IActionResult> ShareOnDb()
{
//get user and plan id using this.AppUserCache (IUserCaching)
var currentUser = await this.AppUserCache.GetLoggedOnUser();
var planId = currentUser.PlanId.Value;
// get plan requirement using this.Plans (IPlansReader)
var isYes = this.Plans[planId, "CanShareInFb"];
if(isYes=="yes"){
//....share on fb
}else{
//let user know his/her plan doesnt support it, may be ask to upgrade
}
}
.....
}
2. Use RequirePlan filter.
If you want to check if a user has a specific plan subscribed to be able to enjoy a feature. Then use the filter like below.
[TypeFilter(typeof(RequiresPlan), Arguments = new object[] { 1 /*plan id*/ }, Order = RequiresPlan.DEFAULT_ORDER)]
public async Task<IActionResult> RenderPdf(){
.......
}
If you need to check user's plan and also want to check an one of its option's value of the plan then follow this example. Lets say there is a plan with id =1 and we need to check if one of the options/features (coming from plan definitions table) of this plan called 'CanDoThat' has a value of '1'. If it is '1' only then the user is allowed to access that particular feature (controller action).
[TypeFilter(typeof(RequiresPlan), Arguments = new object[] { 1 /*plan id*/, "CanDoThat","1" }, Order = RequiresPlan.DEFAULT_ORDER)]
public async Task<IActionResult> RenderPdf(){
.......
}
TODO : Instead of plan id, in future you can supply plan name. I think that would be more convenient. Also I want to be able to pass a list of plan names and options and their values to determine if the user should be allowed to access an action or not.