Building a Mobile Application with Subscription Billing Using Flutter and the Rapyd API

The Rapyd Collect API is a platform where you can easily collect payment from a customer and deposit it into Rapyd Wallets. Use cases for the Rapyd Collect API At the end of this article, you will learn how to make a checkout payment and implement a Rapyd subscription plan in Flutter. What is the…


This content originally appeared on CodeSource.io and was authored by Deven

The Rapyd Collect API is a platform where you can easily collect payment from a customer and deposit it into Rapyd Wallets.

Use cases for the Rapyd Collect API

  • Accepting hundreds of different payment methods in the following categories: bank transfer, bank redirect, card, cash, and local eWallet.
  • Checkout
  • Subscription billing and invoicing
  • Orders and Invoices

At the end of this article, you will learn how to make a checkout payment and implement a Rapyd subscription plan in Flutter.

What is the Rapyd Collect API?

The Rapyd Collect API is a powerful tool that allows businesses to accept customer payments worldwide seamlessly.

It supports a broad spectrum of payment methods, such as credit cards, bank transfers, eWallets, and cash.

This dynamic tool facilitates convenient transactions for enterprise customers regardless of location or preferred payment mode.

Embrace the power of Rapyd Collect API and unlock unparalleled opportunities for global business growth and success.

Rapyd Collect API features:

  • Payments: Payments collect funds into one or more Rapyd wallets.
  • Goods and services: Goods are buyable items that can be incorporated into orders. Services are associated with a plan and can be incorporated into subscriptions.
  • Subscriptions and coupons: A subscription is a recurring payment for services. You can define a subscription with a free trial period or a coupon.
  • Orders and invoices: An order is a one-time charge for goods. It can include separate line items for shipping and taxes. An invoice contains a list of charges from a subscription and defines when the payment is due.
  • Rapyd Checkout: Checkout is a hosted page solution that allows sellers to create payments with the direct involvement of their customers. The hosted page can be embedded in the seller’s own website, or the customer can be redirected to a white-labeled page on Rapyd’s servers.

Implementing payments in a Flutter application

In this section, we will explain how to integrate generated checkout pages and recurring subscription plans in Flutter with the help of Rapyd collect API.

Requirements:

Note: This guide is already quite lengthy, so we have not covered how to create products and plans in Rapid. However, you can find detailed guides and tutorials on the main Rapid API website.

Step 1: Create a Rapyd account

If you already have a Rapyd account, you can skip this step. However, make sure to enable the Sandbox environment so that you do not lose any data.

To create a new Rapyd account, please visit the following website and follow the instructions:

  • https://dashboard.rapyd.net/sign-up

Access keys and Secret Key.

In order to utilize any of Rapyd’s APIs, you will need your access and secret keys as part of your authentication credentials.

To locate these keys, navigate to the “Credential Details” section under “Developers” in the left-hand navigation menu.

Be sure to copy and securely store these keys for future reference in this tutorial.

Creating and Customizing a Rapyd Checkout Page

In order to have a professional look and user experience, it is recommended to customize your checkout page to match the theme of your website. You can also add your own logo or even change the color of the page.

These customizations can be found in the Settings menu on the left navigation of the dashboard under Branding.

There are various options to customize the look and feel of your checkout page, and Rapyd has an extensive guide to customizations.

For more information, you can follow Rapyd’s official guide:

  • https://docs.rapyd.net/client-portal/docs/customizing-your-hosted-page

Step 2 : Setup Flutter app

You can clone our dummy shop app or use your app, It is optional.

To use our app, follow these steps :

Clone App

git clone https://github.com/Dunebook/rapyd_collect_api-main.git

As you can see, there are two branches: Main and dev.

The main branch is what we need to start, and the other one is the completed source code.

To switch to the completed code, you can use this command :

$ git checkout dev 

Our Base App

This app is only UI, without any backend. In the following sections, we will implement the Rapids backend into it.

The app has three tabs: items, plans, and shopping cart.

In the items section, there are some products. After adding them to the cart, the selected items will appear in the shopping cart section.

You can also add, remove, or modify them from there.

The plans section has three plans: basic, pro, and advanced. These plans and all their data, such as price and description, will be loaded from Rapyd’s product API.

After the user clicks the subscribe button, we will redirect them to Rapyd’s checkout page.

Step 3: Implement Rapyd Collect Api

Create a new folder named utils in the lib/core directory. Then, create another folder in the utils folder and call it rapy_client_api.

We will create two Dart files for our API fetching in this folder. The first one is rapyd_client_api_impl.dart, and the second one is rapyd_client_api.dart.

The rapyd_client_api.dart file will contain an abstract class, and rapyd_client_api_impl.dart will extend from it.

We will define our API calls in rapyd_client_api, and write the main code in the rapyd_client_api_impl file.

In the end, your utils folder would look like this:

rapyd_api.png

If you need more details about dart abstract classes, you can follow this link :

Now, open the rapyd_client_api file and write these codes:

abstract class RapydClientApi {
  Future<Map> checkout(String price);
}

As mentioned before, this file is used to define our APIs. Now, open the rapyd_client_api_impl file to implement the main API.

class RapydClientApiImpl extends RapydClientApi {}

When you extend this class from the RapydClientApi class, an error will occur immediately. Click on the quick fix and click “create four missing overrides”. Dart will automatically generate the API codes for you.

Your code should look something like this:

class RapydClientApiImpl extends RapydClientApi {
  @override
  Future<Map> checkout(String price) {
    // TODO: implement checkout
    throw UnimplementedError();
  }


}

That was for our API definitions. Now, let’s dive into implementing API codes.

To connect to Rapyd’s API, we need to define our auth headers in the following steps:

Base data


To access Rapyd’s API, you first need your access key and secret key that you generated in previous sections.

Open the rapyd_client_api_impl.dart file and write this code in the first section of your class:

  final _baseURL = 'https://sandboxapi.rapyd.net/v1';
  final _accessKey = 'Your_Access_Key';
  final _secretKey = 'Your_Secret_Key';

Generating random salt

****Based on the official documentation, requests are accompanied by a randomly generated string of eight to sixteen characters.

This string comprises digits, letters, and special symbols, as the documentation describes.

To achieve this functionality, use the following code snippet (again, write this code in the rapyd_client_api_impl.dart file and don’t forget to import the required libraries):

 String _generateSalt() {
    final _random = Random.secure();
    // Generate 16 characters for salt by //generating 16 random bytes
    // and encoding it.
    final randomBytes = List<int>.generate(16, (index) => _random.nextInt(256));
    return base64UrlEncode(randomBytes);
  }

Building the Headers


According to the official documentation, the header of our request consists of the access key, salt, timestamp, signature, and content type.

  • Access key: Unique access key provided by Rapyd for each authorized user.
  • Content-Type: Indicates that the data appears in JSON format.
  • Salt: Generated in the previous part.
  • Signature: Signature calculated for each request individually.
  • Timestamp: ****Timestamp for the request, in Unix time.

Generate Header Code

Write this code in the rapyd_client_api_impl.dart file. Don’t forget to import the required libraries.
This code will generate the necessary headers for Rapyd.

Map<String, String> _generateHeader({
    required String method,
    required String endpoint,
    String body = '',
  }) {
    int unixTimetamp = DateTime.now().millisecondsSinceEpoch;
    String timestamp = (unixTimetamp / 1000).round().toString();

    var salt = _generateSalt();

    var toSign =
        '$method/v1$endpoint$salt$timestamp$_accessKey$_secretKey$body';

    print(toSign);

    var keyEncoded = ascii.encode(_secretKey);
    var toSignEncoded = ascii.encode(toSign);

    var hmacSha256 = Hmac(sha256, keyEncoded); // HMAC-SHA256
    var digest = hmacSha256.convert(toSignEncoded);
    var ss = hex.encode(digest.bytes);
    var tt = ss.codeUnits;
    var signature = base64.encode(tt);

    var headers = {
      'Content-Type': 'application/json',
      'access_key': _accessKey,
      'salt': salt,
      'timestamp': timestamp,
      'signature': signature,
    };

    return headers;
  }

This was the basic setup for preparing our requests. Next, we will write our APIs for two methods:

  • Method 1: Hosted Checkout Page
  • Method 2: Subscription and recurring plans.

Method 1: Hosted Checkout Page

This method is used for one-time payments, such as when you buy clothes online. It will only charge the buyer once, with no recurring billings.

First, we need to define our model. Create a models folder in the lib\core directory and create a base_response.dart file.

I created this model using the generated JSON code from the Rapyd API JSON output and from this site.

Now, write the generated code to the base_response.dart file:

class BaseResponse {
  Status? status;
  dynamic? data;

  BaseResponse({this.status, this.data});

  BaseResponse.fromJson(Map<String, dynamic> json) {
    status =
        json['status'] != null ? new Status.fromJson(json['status']) : null;
    if (json['data'] != null) {
      data = json['data'];
    }
  }
}

class Status {
  String? errorCode;
  String? status;
  String? message;
  String? responseCode;
  String? operationId;

  Status(
      {this.errorCode,
      this.status,
      this.message,
      this.responseCode,
      this.operationId});

  Status.fromJson(Map<String, dynamic> json) {
    errorCode = json['error_code'];
    status = json['status'];
    message = json['message'];
    responseCode = json['response_code'];
    operationId = json['operation_id'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['error_code'] = this.errorCode;
    data['status'] = this.status;
    data['message'] = this.message;
    data['response_code'] = this.responseCode;
    data['operation_id'] = this.operationId;
    return data;
  }
}

This code will help us parse the JSON output more easily.

According to the primary documentation, for generating checkout pages, we need a body:

 {
      "amount": 5.99,
      "currency": "USD",
      "country": "US",
      "complete_checkout_url":"https://example.com/thankyou",
      "cancel_checkout_url":"https://example.com/error"
    }
  • Amount : Price of Product.
  • Complete_checkout_url : Url to redirect when payment is successful.
  • Cancel_checkout_url : Url to redirect when payment is not successful.

Now, we need to write the code for the checkout method. First, we need to import the base response model and the HTTP library:

import 'package:rapyd_collect_api/core/utils/rapyd_client_api.dart';
import 'package:http/http.dart' as http;  // using for http requests

Next, we should send the request. To do this, we define the body in the same way as we previously described using the Rapyd API. Then, we specify the method as a POST request. Afterwards, we include the endpoint, which is /checkout. Finally, if the request is successful, we will return a map.

Full code :

  @override
  Future<Map> checkout(String price) async {
    final checkoutBody = <String, String>{
      "amount": price,
      "currency": "USD",
      "country": "US",
      "complete_checkout_url": "https://example.com/thankyou",
      "cancel_checkout_url": "https://example.com/error"
    };
    var method = "post";
    var rapydEndpoint = '/checkout';

    final walletURL = Uri.parse(_baseURL + rapydEndpoint);

    final headers = _generateHeader(
      method: method,
      endpoint: rapydEndpoint,
      body: jsonEncode(checkoutBody),
    );

    try {
      var response = await http.post(walletURL,
          headers: headers, body: jsonEncode(checkoutBody));

      print(response.body);

      final baseResponse = BaseResponse.fromJson(jsonDecode(response.body));

      if (response.statusCode == 200) {
        print('checkout done successfully!');
        return baseResponse.data;
      } else {
        throw Exception(response.body);
      }
    } catch (_) {
      print(_);
      print('Failed to subscribe');
    }
    throw Exception('Failed to checkout');
  }

Note: Replace this code with the Dart code generated in the first part.

Create the CartCheckoutScreen Page

****To be able to implement a checkout screen, we need to add the *Flutter WebView* package:

$ flutter pub add "webview_flutter"

For Android builds, you must set the minimum minSdkVersion in android/app/build.gradle to 19, as seen below:

android {
    defaultConfig {
        minSdkVersion 19 
    }
}

Cart Checkout Page Code:

Create a file called **cart_checkout_page.dart** in the screen folder and copy this snippet into it.

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rapyd_collect_api/core/utils/rapyd_client_api.dart';
import 'package:rapyd_collect_api/core/utils/rapyd_client_api_impl.dart';
import 'package:rapyd_collect_api/src/controller/office_furniture_controller.dart';
import 'package:webview_flutter/webview_flutter.dart';
class CartCheckoutPage extends StatelessWidget {
  CartCheckoutPage({super.key});
  final controller = Get.put(OfficeFurnitureController());
  final RapydClientApi rapyd = RapydClientApiImpl();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: FutureBuilder(
      future: rapyd.checkout(controller.totalPrice.value.toString()),
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
            return const Center(child: CircularProgressIndicator());
          default:
            if (snapshot.hasError) {
              return const Center(child: Text('Some error occurred!'));
            } else {
              var webViewController = WebViewController()
                ..setJavaScriptMode(JavaScriptMode.unrestricted)
                ..setNavigationDelegate(
                  NavigationDelegate(
                    onProgress: (int progress) {
                      // Update loading bar.
                    },
                    onPageStarted: (String url) {
                      if (url
                          .contains(snapshot.data!["complete_checkout_url"])) {
                        Get.back(result: 'Success');
                        controller.clearCart();
                        Get.showSnackbar(GetSnackBar(
                          title: 'Success',
                          message: 'Success',
                        ));
                      } else if (url
                          .contains(snapshot.data!["cancel_checkout_url"])) {
                        Get.back(result: 'Canceled');
                        controller.clearCart();
                        Get.showSnackbar(GetSnackBar(
                          title: 'Failure',
                          message: 'Failure',
                        ));
                      }
                    },
                    onPageFinished: (String url) {},
                    onWebResourceError: (WebResourceError error) {},
                  ),
                )
                ..loadRequest(
                    Uri.parse(snapshot.data!["redirect_url"].toString()));
              return WebViewWidget(
                controller: webViewController,
              );
            }
        }
      },
    ));
  }
}

To begin, we will initialize our controller. Then, we will create a future builder and utilize the API we have previously written.

Once we have connected to the future, we will use a web view with the rapid checkout URL from our API response.

Additionally, we will check if the payment was successful and display a successful snack bar to the user. If the payment was unsuccessful, we will display an unsuccessful snack bar.
Please keep in mind that the future response will be in the form of a map.

Handling checkout button .


Open the cart_screen.dart file and write this code in the onTap section (Don’t forget to import CartCheckoutPage):

controller.totalPrice > 0
? () {
Get.to(CartCheckoutPage());
}
: null,

The full code would look like this :

bottomNavigationBar: Obx(
() {
return BottomBar(
priceLabel: "Total price",
priceValue: "\$${controller.totalPrice.value.toStringAsFixed(2)}",
buttonLabel: "Checkout",
onTap: controller.totalPrice > 0
? () {
Get.to(CartCheckoutPage());
}
: null,
);
},

We will redirect the user to the checkout screen with this code.

Now, go ahead and run the code. You should be able to do a simple checkout!

Method 2 : Subscription and recurring plans.

Before writing functions for our methods, we must create models for our app to understand things better and have cleaner code, as we did for the base_response model.

Create two files in the core/models folder: plan.dart and product.dart.

code for Product.dart:

class Product {
  String? id;
  bool? active;
  List<String>? attributes;
  int? createdAt;
  String? description;
  List<String>? images;
  Metadata? metadata;
  String? name;
  PackageDimensions? packageDimensions;
  bool? shippable;
  List<Null>? skus;
  String? statementDescriptor;
  String? type;
  String? unitLabel;
  int? updatedAt;

  Product(
      {this.id,
      this.active,
      this.attributes,
      this.createdAt,
      this.description,
      this.images,
      this.metadata,
      this.name,
      this.packageDimensions,
      this.shippable,
      this.skus,
      this.statementDescriptor,
      this.type,
      this.unitLabel,
      this.updatedAt});

  Product.fromJson(Map<String, dynamic> json) {
    id = json['id'];
    active = json['active'];
    attributes = json['attributes'].cast<String>();
    createdAt = json['created_at'];
    description = json['description'];
    images = json['images'].cast<String>();
    metadata = json['metadata'] != null
        ? new Metadata.fromJson(json['metadata'])
        : null;
    name = json['name'];
    packageDimensions = json['package_dimensions'] != null
        ? new PackageDimensions.fromJson(json['package_dimensions'])
        : null;
    shippable = json['shippable'];

    statementDescriptor = json['statement_descriptor'];
    type = json['type'];
    unitLabel = json['unit_label'];
    updatedAt = json['updated_at'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['id'] = this.id;
    data['active'] = this.active;
    data['attributes'] = this.attributes;
    data['created_at'] = this.createdAt;
    data['description'] = this.description;
    data['images'] = this.images;
    if (this.metadata != null) {
      data['metadata'] = this.metadata!.toJson();
    }
    data['name'] = this.name;
    if (this.packageDimensions != null) {
      data['package_dimensions'] = this.packageDimensions!.toJson();
    }
    data['shippable'] = this.shippable;

    data['statement_descriptor'] = this.statementDescriptor;
    data['type'] = this.type;
    data['unit_label'] = this.unitLabel;
    data['updated_at'] = this.updatedAt;
    return data;
  }
}

class Metadata {
  Metadata.fromJson(Map<String, dynamic> json) {}

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    return data;
  }
}

class PackageDimensions {
  int? height;
  int? length;
  int? weight;
  int? width;

  PackageDimensions({this.height, this.length, this.weight, this.width});

  PackageDimensions.fromJson(Map<String, dynamic> json) {
    height = json['height'];
    length = json['length'];
    weight = json['weight'];
    width = json['width'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['height'] = this.height;
    data['length'] = this.length;
    data['weight'] = this.weight;
    data['width'] = this.width;
    return data;
  }
}

Our code defines three classes: Product, Metadata, and PackageDimensions. The Product class represents a product in a store. It has 13 properties, including the product’s id, name, description, images, and dimensions.

The Metadata class is a nested class within Product. It is used to store metadata about the product, such as the product’s brand, model, and color.

The PackageDimensions class is also a nested class within Product. It is used to store the dimensions of the product, such as its height, width, length, and weight.

code for plan.dart :

import 'package:rapyd_collect_api/core/models/product.dart';
class Plan {
  String? id;
  String? aggregateUsage;
  String? amount;
  String? billingScheme;
  int? createdAt;
  String? currency;
  String? interval;
  int? intervalCount;
  Metadata? metadata;
  Product? product;
  String? nickname;
  List<Tiers>? tiers;
  String? tiersMode;
  TransformUsage? transformUsage;
  int? trialPeriodDays;
  String? usageType;
  Plan(
      {this.id,
      this.aggregateUsage,
      this.amount,
      this.billingScheme,
      this.createdAt,
      this.currency,
      this.interval,
      this.intervalCount,
      this.metadata,
      this.product,
      this.nickname,
      this.tiers,
      this.tiersMode,
      this.transformUsage,
      this.trialPeriodDays,
      this.usageType});
  Plan.fromJson(Map<String, dynamic> json) {
    id = json['id'];
    aggregateUsage = json['aggregate_usage'];
    amount = json['amount'].toString();
    billingScheme = json['billing_scheme'];
    createdAt = json['created_at'];
    currency = json['currency'];
    interval = json['interval'];
    intervalCount = json['interval_count'];
    metadata = json['metadata'] != null
        ? new Metadata.fromJson(json['metadata'])
        : null;
    product =
        json['product'] != null ? new Product.fromJson(json['product']) : null;
    nickname = json['nickname'];
    if (json['tiers'] != null) {
      tiers = <Tiers>[];
      json['tiers'].forEach((v) {
        tiers!.add(new Tiers.fromJson(v));
      });
    }
    tiersMode = json['tiers_mode'];
    transformUsage = json['transform_usage'] != null
        ? new TransformUsage.fromJson(json['transform_usage'])
        : null;
    trialPeriodDays = json['trial_period_days'];
    usageType = json['usage_type'];
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['id'] = this.id;
    data['aggregate_usage'] = this.aggregateUsage;
    data['amount'] = this.amount;
    data['billing_scheme'] = this.billingScheme;
    data['created_at'] = this.createdAt;
    data['currency'] = this.currency;
    data['interval'] = this.interval;
    data['interval_count'] = this.intervalCount;
    if (this.metadata != null) {
      data['metadata'] = this.metadata!.toJson();
    }
    if (this.product != null) {
      data['product'] = this.product!.toJson();
    }
    data['nickname'] = this.nickname;
    if (this.tiers != null) {
      data['tiers'] = this.tiers!.map((v) => v.toJson()).toList();
    }
    data['tiers_mode'] = this.tiersMode;
    if (this.transformUsage != null) {
      data['transform_usage'] = this.transformUsage!.toJson();
    }
    data['trial_period_days'] = this.trialPeriodDays;
    data['usage_type'] = this.usageType;
    return data;
  }
}
class Metadata {
  Metadata();
  Metadata.fromJson(Map<String, dynamic> json) {}
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    return data;
  }
}
class PackageDimensions {
  int? height;
  int? length;
  int? weight;
  int? width;
  PackageDimensions({this.height, this.length, this.weight, this.width});
  PackageDimensions.fromJson(Map<String, dynamic> json) {
    height = json['height'];
    length = json['length'];
    weight = json['weight'];
    width = json['width'];
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['height'] = this.height;
    data['length'] = this.length;
    data['weight'] = this.weight;
    data['width'] = this.width;
    return data;
  }
}
class Tiers {
  double? amount;
  String? upTo;
  int? flatAmount;
  Tiers({this.amount, this.upTo, this.flatAmount});
  Tiers.fromJson(Map<String, dynamic> json) {
    amount = json['amount'];
    upTo = json['up_to'];
    flatAmount = json['flat_amount'];
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['amount'] = this.amount;
    data['up_to'] = this.upTo;
    data['flat_amount'] = this.flatAmount;
    return data;
  }
}
class TransformUsage {
  int? divideBy;
  String? round;
  TransformUsage({this.divideBy, this.round});
  TransformUsage.fromJson(Map<String, dynamic> json) {
    divideBy = json['divide_by'];
    round = json['round'];
  }
  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['divide_by'] = this.divideBy;
    data['round'] = this.round;
    return data;
  }
}

We need this model to parse our JSON response from Rapyd API’s REST API. You can ignore this and use the map output, but this is a cleaner solution.

After defining our models, we must write some code for our requests! So, open the rapyd_client_api file. We are going to declare the getPlanDetails, getPlans, and subscribeToPlan methods.

import 'package:rapyd_collect_api/core/models/plan.dart';
abstract class RapydClientApi {
  Future<List<Plan>> getPlans();
  Future<Plan> getPlanDetails(String planId);
  Future<Map> subscribeToPlan(String planId);
  Future<Map> checkout(String price);
}

Note: You may see an error showing, but don’t worry, we will fix them later.
Now, let’s define them!

Start with the getPlans method. This method will be used to get all the plan names and IDs we have on the Rapyd server.

After getting plan IDs, we will use this ID on the getPlanDetails method to get details like price, description, etc.

Note: Remember to import models and libraries after copying the code.
Here is the complete code:

  @override
  Future<List<Plan>> getPlans() async {
    List<Plan> retrievedPlans = [];
    var method = "get";
    var rapydEndpoint = '/plans';
    final walletURL = Uri.parse(_baseURL + rapydEndpoint);
    final headers = _generateHeader(
      method: method,
      endpoint: rapydEndpoint,
    );
    print(walletURL);
    try {
      var response = await http.get(walletURL, headers: headers);
      print(response.body);
      if (response.statusCode == 200) {
        print('plans retrieved successfully!');
        final baseResponse = BaseResponse.fromJson(jsonDecode(response.body));
        for (var plan in baseResponse.data) {
          retrievedPlans.add(Plan.fromJson(plan));
        }
        return retrievedPlans;
      } else {
        throw Exception(response.body);
      }
    } catch (_) {
      print(_);
      print('Failed to retrieve plan list');
    }
    throw Exception('Failed to get plans ');
  }

This method will return a list of plans using the get request.

Note: If you have not defined any plan on your Rapyd account, you will receive an empty list!
In order to create one, you can follow this guide:

Now, let’s define the following method, getPlanDetails:

  @override
  Future<Plan> getPlanDetails(String planId) async {
    Plan? retrievedPlanDetails;

    var method = "get";
    var rapydEndpoint = '/plans/$planId';

    final walletURL = Uri.parse(_baseURL + rapydEndpoint);

    final headers = _generateHeader(
      method: method,
      endpoint: rapydEndpoint,
    );

    try {
      var response = await http.get(walletURL, headers: headers);

      if (response.statusCode == 200) {
        print('plans retrieved successfully!');

        final baseResponse = BaseResponse.fromJson(jsonDecode(response.body));

        retrievedPlanDetails = Plan.fromJson(baseResponse.data);

        return retrievedPlanDetails!;
      } else {
        throw Exception(response.body);
      }
    } catch (_) {
      print('Failed to retrieve plan');
    }

    throw Exception('Unable to get data');
  }

Using the planId object we received from the last method, we will pass this to Rapyd, and in response, we will receive all the plan details!

subscribeToPlan method:

This method will be used to subscribe the selected plan to the user.

 @override
  Future<Map> subscribeToPlan(String planId) async {
    Map subscribeBody = {
      "customer": "cus_157f87d8508724c7e8fd4a5abe28cc34",
      "merchant_reference_id": "acct_111197",
      "country": "us",
      "language": "US",
      "billing": "pay_automatically",
      "cancel_at_period_end": true,
      "coupon": "",
      "payment_fees": {
        "fx_fee": {"calc_type": "net", "fee_type": "percentage", "value": 0}
      },
      "payment_method_": "ewallet",
      "subscription_items": [
        {"plan": planId, "quantity": 1}
      ],
      "metadata": {"merchant_defined": true},
      "tax_percent": 0,
      "complete_payment_url": "https://example.com/thankyou",
      "error_payment_url": "https://example.com/error",
      "cancel_url": "https://example.com/error",
      "complete_url": "https://example.com/thankyou",
      "page_expiration": 1699470811,
      "trial_from_plan": false,
      "trial_period_days": null
    };
    var method = "post";
    var rapydEndpoint = '/checkout/subscription';
    final walletURL = Uri.parse(_baseURL + rapydEndpoint);
    print('wallet url $walletURL');
    final headers = _generateHeader(
      method: method,
      endpoint: rapydEndpoint,
      body: jsonEncode(subscribeBody),
    );
    try {
      var response = await http.post(walletURL,
          headers: headers, body: jsonEncode(subscribeBody));
      print(response.body);
      final baseResponse = BaseResponse.fromJson(jsonDecode(response.body));
      if (response.statusCode == 200) {
        print('subscribed successfully!');
        return baseResponse.data;
      } else {
        throw Exception(response.body);
      }
    } catch (_) {
      print(_);
      print('Failed to subscribe');
    }
    throw Exception('Failed to subscribe');
  }

Body:

Customer: The customer who is subscribing. You can generate a customer with this guide on the main documentation (preferably, it is generated when someone registers on your app):

  • You can remove it if you want to avoid creating a customer.
  • cancel_checkout_url : Url to redirect when the payment is canceled.
  • complete_checkout_url :Url to redress when the payment is completed.

Method and endpoints are based on what the official guide says:

Now that our methods are ready, we can dive into the UI part!

In the lib\src\controller folder, create the file and name it plans_controller.dart.

We will use this GetX controller to manage our state.

Copy this code into it:

import 'package:get/get.dart';
import 'package:rapyd_collect_api/core/models/plan.dart';
import 'package:rapyd_collect_api/core/utils/rapyd_client_api.dart';
import 'package:rapyd_collect_api/core/utils/rapyd_client_api_impl.dart';
class PlansController extends GetxController
    with GetSingleTickerProviderStateMixin {
  final planDetails = <Plan>[].obs;
  final loadingStatus = LoadingStatus.init.obs;
  final RapydClientApi rapyd = RapydClientApiImpl();
}
enum LoadingStatus { init, loading, failure, success }

We declared some variables here:

  • Plan details: We will store our plan details in this variable.
  • Loading status: This variable will save our loading status.
  • Rapid: We can access the API we wrote by this object.

Now, we will declare the **loadPlans** method. This method will use a Rapyd API class to send requests to the server. After receiving the plan details, we will send another request to get product details.

  loadPlanIds() async {
    loadingStatus(LoadingStatus.loading);
    try {
      final plans = await rapyd.getPlans();
// Getting product details
      for (var plan in plans) {
        print('plan product:\n${plan.product}');
        print('plan nickname:\n${plan.nickname}');
        final result = await rapyd.getPlanDetails(plan.id!);
        planDetails.add(result);
      }
      loadingStatus(LoadingStatus.success);
    } on Exception catch (e) {
      loadingStatus(LoadingStatus.failure);
    }
  }

In order to load plans, we need to send a request to the API when our UI loads. So, declare the onInit method in the controller.

Any code you put here will load immediately after the widget is allocated in memory.

  @override
  void onInit() async {
    print('initiated on init');

    await loadPlanIds();

    super.onInit();
  }

That was for our controller class. Now, it’s time for the view. First, let’s create our subscription checkout page.

It’s similar to cart_checkout_page.dart but slightly different.

Make a Dart file in the screen folder, call it subscription_checkout_page.dart, and copy this code into it:

Code:

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:rapyd_collect_api/src/controller/plans_controller.dart';
import 'package:webview_flutter/webview_flutter.dart';
class SubscriptionCheckoutPage extends StatelessWidget {
  SubscriptionCheckoutPage({super.key, required this.planId});
  final String planId;
  final contronller = Get.put(PlansController());
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: FutureBuilder(
      future: contronller.rapyd.subscribeToPlan(planId),
      builder: (context, snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.waiting:
            return const Center(child: CircularProgressIndicator());
          default:
            if (snapshot.hasError) {
              return const Center(child: Text('Some error occurred!'));
            } else {
              var webViewController = WebViewController()
                ..setJavaScriptMode(JavaScriptMode.unrestricted)
                ..setNavigationDelegate(
                  NavigationDelegate(
                    onProgress: (int progress) {
                      // Update loading bar.
                    },
                    onPageStarted: (String url) {
                      if (url
                          .contains(snapshot.data!["complete_checkout_url"])) {
                        Get.back(result: 'Success');
                        Get.showSnackbar(GetSnackBar(
                          title: 'Success',
                          message: 'Success',
                        ));
                      } else if (url
                          .contains(snapshot.data!["cancel_checkout_url"])) {
                        Get.back(result: 'Canceled');
                        Get.showSnackbar(GetSnackBar(
                          title: 'Failure',
                          message: 'Failure',
                        ));
                      }
                    },
                    onPageFinished: (String url) {},
                    onWebResourceError: (WebResourceError error) {},
                  ),
                )
                ..loadRequest(
                    Uri.parse(snapshot.data!["redirect_url"].toString()));
              return WebViewWidget(
                controller: webViewController,
              );
            }
        }
      },
    ));
  }
}

The main difference between this and other checkout files is that it has a planId. We need this for subscribing to the plan.
The next part is creating a plan screen. Open the plans_screen.dart file and delete the plans variable and initialize the controller:

 final controller = Get.put(PlansController());

Instead of our dummy data, we will use actual data from our Rapyd API.

New code :

import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:rapyd_collect_api/src/controller/plans_controller.dart';
import 'package:rapyd_collect_api/src/view/widget/plan_card.dart';
import 'subscription_checkout_page.dart';
class PlansScreen extends StatefulWidget {
  const PlansScreen({Key? key}) : super(key: key);
  @override
  State<PlansScreen> createState() => _PlansScreenState();
}
class _PlansScreenState extends State<PlansScreen> {
  final controller = Get.put(PlansController());
  final ScrollController _scrollController =
      ScrollController(initialScrollOffset: Get.width * .98);
  @override
  void initState() {
    super.initState();
  }
  @override
  Widget build(BuildContext context) {
    return Obx(() {
      return Scaffold(
        body: _renderBody(),
      );
    });
  }
  Widget _renderBody() {
    switch (controller.loadingStatus.value) {
      case LoadingStatus.failure:
        return const Center(
          child: Text('Failed to retrieve plans\nCheck your connection '),
        );
      case LoadingStatus.success:
        return _planLists();
      case LoadingStatus.init:
        return const Center(
          child: CircularProgressIndicator(),
        );
      case LoadingStatus.loading:
        return const Center(
          child: CircularProgressIndicator(),
        );
    }
  }
  ListView _planLists() {
    return ListView.builder(
      reverse: true,
      controller: _scrollController,
      itemBuilder: (context, index) {
        final plan = controller.planDetails[index];
        return PlanCard(
            title: plan.product!.name!,
            subTitle: plan.amount!,
            details: plan.product!.description!,
            onTap: () {
              final result = Get.to(SubscriptionCheckoutPage(
                planId: plan.id!,
              ));
              print(result);
            });
      },
      itemCount: controller.planDetails.length,
      scrollDirection: Axis.horizontal,
    );
  }
}

You can see. First, we will load plans, and after that, if the user clicks on subscribe plan, we will direct him to the checkout page.

Now you can run the code !

Note : if you receive this error :

timestamp header is out of allowed range

This means your clock time is not correct.

Conclusion

In this article, we made a Flutter app and implemented the Rapyd Collect API. We used the HTTP package to send requests. We also learned how to subscribe to a plan and get a list of plans. We also learned how to make a checkout page with the help of the webview_flutter package.
After this article, you can explore the Rapyd documentation and be confident enough to make your API requests. Rapyd also has other useful features, which you can find here.


This content originally appeared on CodeSource.io and was authored by Deven


Print Share Comment Cite Upload Translate Updates
APA

Deven | Sciencx (2023-08-15T14:39:25+00:00) Building a Mobile Application with Subscription Billing Using Flutter and the Rapyd API. Retrieved from https://www.scien.cx/2023/08/15/building-a-mobile-application-with-subscription-billing-using-flutter-and-the-rapyd-api/

MLA
" » Building a Mobile Application with Subscription Billing Using Flutter and the Rapyd API." Deven | Sciencx - Tuesday August 15, 2023, https://www.scien.cx/2023/08/15/building-a-mobile-application-with-subscription-billing-using-flutter-and-the-rapyd-api/
HARVARD
Deven | Sciencx Tuesday August 15, 2023 » Building a Mobile Application with Subscription Billing Using Flutter and the Rapyd API., viewed ,<https://www.scien.cx/2023/08/15/building-a-mobile-application-with-subscription-billing-using-flutter-and-the-rapyd-api/>
VANCOUVER
Deven | Sciencx - » Building a Mobile Application with Subscription Billing Using Flutter and the Rapyd API. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2023/08/15/building-a-mobile-application-with-subscription-billing-using-flutter-and-the-rapyd-api/
CHICAGO
" » Building a Mobile Application with Subscription Billing Using Flutter and the Rapyd API." Deven | Sciencx - Accessed . https://www.scien.cx/2023/08/15/building-a-mobile-application-with-subscription-billing-using-flutter-and-the-rapyd-api/
IEEE
" » Building a Mobile Application with Subscription Billing Using Flutter and the Rapyd API." Deven | Sciencx [Online]. Available: https://www.scien.cx/2023/08/15/building-a-mobile-application-with-subscription-billing-using-flutter-and-the-rapyd-api/. [Accessed: ]
rf:citation
» Building a Mobile Application with Subscription Billing Using Flutter and the Rapyd API | Deven | Sciencx | https://www.scien.cx/2023/08/15/building-a-mobile-application-with-subscription-billing-using-flutter-and-the-rapyd-api/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.