Pabulo Platform Dart Library

Pábulo: The future of Loyalty, Merchants & Wallet Platforms, Today.

Provides APIs for seamlessly connect to the Pabulo Platform and build rich Flutter UIs in no time.

Motivation


✅ To provide ability to quickly build front-ends in the shortest possible time.
✅ Simplifying complex JSON data models into standard Request & Response objects.
✅ Providing efficiency to reduce network traffic by validating data before doing a server round trip.

Get Started


Future<void> main() async {
  // Your code here....
  /// Initialize the Globals for the base framework.
  /// This is a singleton so enable it in the main
  /// for early initialization.
  PabuloGlobals(
    baseUrl: 'https://api.domain.com:8080',
    requestTimeOut: Duration(seconds: 30),
    language: 'en-US',
    apiKey: 'NDM6f6dd3460b0f4a90b.....',
  );
  // You code after this.
}

Initialize the PabuloGlobals() with the parameters:

  1. baseUrl : this is the path to the backend where the core platform is running , i.e. https://api.domain.com or https://api.domain.com:8080
  2. requestTimeOut : Optional The time as Durations to wait for backend to respond. If ignored the default is 30 seconds.
  3. language : Optional This is the language 'en-US' or 'fr-FR' to be used for communicating to the backand. If ignored the default is 'en-US'.
  4. apiKey : Optional This is the API Key that is assigned by the backend administrator to perform special operations that requires elevated security.

🫡 That is all !!! You are now wired up to start using the SDK.

Usage


Once you have wired the SDK in the main() the next step is to use the repositories{^1} for the business flow that you are working on.

Use case 1: you are building a Login Screen for users to login.

class LoginBloc extends Bloc<LoginEvent, LoginState> {
  // Wire the SDK Repository for Administration Functions.
  final RepoAdmin _repoAdmin = RepoAdmin();

  // On Login Pressed call the api
  Future<void> onLoginPressed() async {
    try {
      RspLogin? rspLogin = await _repoAdmin.letMeIn(
        ReqLogin(consumerEmail: email, consumerPassword: password),);
    } catch (error) {
      // ... handle errors
    }
    // ... reset of your code.
  }
}

Use case 2: you are building a list of items to show with a input field.

/// By extending the DTO objects from the SDK you can now add 
/// input fields on top of the backend response.
class LoyaltySpendData extends LoyaltySpendDto {
  late TextEditingController minimumSpendAmountController;
  late TextEditingController minimumSpendPointsController;
  late TextEditingController incrementSpendAmountController;
  late TextEditingController incrementSpendPointsController;

  LoyaltySpendData({
    super.currency,
    super.incrementSpendAmount,
    super.incrementSpendPoints,
    super.minimumSpendAmount,
    super.minimumSpendPoints,
    super.symbol,
  }) {
    minimumSpendAmountController = TextEditingController(
      text: minimumSpendAmount?.toString() ?? "",
    );
    minimumSpendPointsController = TextEditingController(
      text: minimumSpendPoints?.toString() ?? "",
    );
    incrementSpendAmountController = TextEditingController(
      text: incrementSpendAmount?.toString() ?? "",
    );
    incrementSpendPointsController = TextEditingController(
      text: incrementSpendPoints?.toString() ?? "",
    );
  }
}

Concepts & Conventions


Naming conventions:

  1. Request Objects: Objects that are used as part of the POST, PUT, PATCH operations to send data to the backend begin with a prefix Req and these classes are under the path src/models/request.
  2. Response Objects: Objects that receive the data as part of the response.body begin with a prefix Rsp and these classes are under the path src/models/response.
  3. Data Transfer Objects (DTO): are objects that hold the actual data, they are part of the Request and Response objects, these classes are suffixed with Dto and implement valid() and copyWith() functionality or additional helper functions to simplify the use of these objects, and these classes are under the path src/models/factory.
  4. Repositories: these are API functions that directly communicate with backend without you needing to understand the security, uri end-point parameters etc., these classes are prefixed with Repo and are under the path src/repositories.
  5. Uri: Universal Resource Indicator are classes with static methods that help you call the backend directly if there is a need to bypass the SDK repo operations, these classes begin with a prefix Uri and follow by the Swagger grouping name. e.g. UriConsumer will have all the end-points related to consumer interactions.{^2} Under the Uri classes there are two types of static definitions:
    1. uri => statics starting with prefix uri are end-points that do not have RequestParam in the uri.
    2. url => statics starting with prefix url are end-points that will need additional ReqestParam to call the backend.

example of URI vs URL:

class UriConsumer {
  static Uri urlIsEmailExists({required String email}) =>
      Uri.parse('$_rootBaseUri/is-duplicate?consumerEmail=$email');

  static final Uri uriReferFriend = Uri.parse('$_rootBaseUri/referral');
}

Concepts

The SDK is build keeping in mind the KISS principles with a standard Input=> Process=> Output paradigm.

  1. Show a screen to user to interact with.
  2. Collect the data in a Request.
  3. Call the respective repository function with the request.
  4. Receive a Response back from the server.

Request: are objects that define the business input, each request will be different as it will be closely tied with the business operation and the data that the backend is expecting.

e.g.: a request to create a new user (sign-up)

{
  "consumerEmail": "string",
  "loginPassword": "string",
  "mobilePhone": "string",
  "fullName": "string",
  "age": 100,
  "interests": [
    "string"
  ],
  "referralCode": "string"
}

the corresponding request object for this is: ReqConsumerSignUp, so the above request would just be:

class SignUpBloc extends Bloc<SignUpEvent, SignUpState> {
  // Wire the SDK Repository for Consumer Functions.
  final RepoConsumer _repoConsumer = RepoConsumer();

  // On Sign Up Pressed call the api
  Future<ReqConsumerSignUp?> onSignUpPressed() async {
    try {
      RspConsumerSignUp? rspConsumerSignUp = await _repoConsumer.createUser(
        ReqConsumerSignUp(consumerEmail: email,
            loginPassword: password,
            mobilePhone: mobilePhone,
            fullName: fullName),);
    } catch (error) {
      // ... handle errors
    }
    // ... reset of your code.
  }
}

Security

The Pabulo platform makes use of the JSON Web Token technology to manage access control.

As a front-end developer, there is no need for you to manage or take care of the tokens or store the tokens within your application. The SDK will manage the lifecycle of the JWT token including refreshing and keeping the JWT token alive. Depending on the platform Android, iOS or Web the SDK will store the JWT token in a appropriate secured storage, and retrieve it as needed.

In order to start the lifecycle of the JWT token you will need to call RepoAdmin.letMeIn() once for the first time there on the SDK will manage the JWT. If this behaviour is not required, then you can issue a RepoAdmin.logOut() to reset the SDK for a new JWT token or you can call RepoAdmin.letMeIn() with different credentials to reset the SDK.

The SDK offer three basic operations upon which you can decide to show a login screen or not:

  1. RepoAdmin.letMeIn() create or update a JWT token, also the 🏃🏽‍➡️ START of the JWT lifecycle management.
  2. isLoginSuccess(){^3} check if there is a valid JWT Token, if true continue, if false show the login screen, get the user credentials and call RepoAdmin.letMeIn().
  3. logOut() {^3} explicitly remove the cached JWT token. 🚫 This is final, once the JWT is removed from the cache, you will need to issue letMeIn().

Events

The SDK comes with its own Event Bus architecture, where you can subscribe to events and take actions. Events are built upon a standard Abstract data model with minimum fields

  1. message : to hold the message.
  2. timeOfEvent : the date and time this event occurred.
  3. source : Source object or any other object to pass at the time of event.
abstract class AbstractEvent {
  /// Date Time of the event
  final DateTime? timeOfEvent;

  /// Message that is set at the time of event
  final String? message;

  /// Source object or any other object to pass at the time of event.
  final Object? source;

  AbstractEvent({this.message, this.source, DateTime? timeOfEvent})
      : this.timeOfEvent = timeOfEvent ?? DateTime.now();
}

To subscribe to events and take necessary actions; you will only need to do:

// Somewhere in your application
EventBus eventBus = PabuloGlobals().globalEventBus;

/// Listen to specific events
eventBus.on<JwtRefreshSuccessEvent>().listen((event) {
   print(event.newJwtToken);
});

// Listen to all events.
eventBus.on().listen((event) {
if (event is JwtRefreshSuccessEvent) {
   print(event.newJwtToken)
   }
});

Error Handling

The SDK is built around principles of try..catch architecture, making sure your application is capable of handling errors and taking necessary steps.

There is only one exception thrown through out the SDK, keeping it simple from your side to catch, however there are different scenarios that will throw the ApiException.

  1. Errors that are caught by underline conditions or operations and rethrown as ApiException or new throw ApiException().
  2. Errors that are converted from the response.status from the backend.
  3. Errors that are thrown when valid() or data validation fails.

eg.: Validation errors that are thrown by the SDK.

class ReqCashWithdrawal extends AbstractBaseModel<ReqCashWithdrawal> implements AbstractValidator {
  @JsonKey(name: 'money', defaultValue: null)
  MoneyDto money;

  @JsonKey(name: 'about')
  String about;

  /// Setters / Getters &amp; Factor Methods removed for readability.

  @override
  void valid() {
    money.valid();
    if (money.amount <= 0) {
      throw ApiException(
        exception: RspApiException(
          message: 'Amount for cash withdrawal must be positive greater than zero.',
          stackTrace: StackTrace.current,
        ),
      );
    }
    if (about.isEmpty) {
      throw ApiException(
        exception: RspApiException(
          message: 'About is required for cash withdrawal.',
          stackTrace: StackTrace.current,
        ),
      );
    }
  }
}

e.g.: A HTTP client error

  Future<Map<String, dynamic>?> httpAuthDelete({
  required Uri url,
  Map<String, String>? apiHeaders,
}) async {
  try {
    /// Code removed for better reliability.
    final Response response = await http
        .delete(url, headers: headers)
        .timeout(_pabuloGlobals!.requestTimeOut);

    if (response.statusCode >= 200 && response.statusCode < 300) {
      return jsonDecode(response.body);
    }
    // Client Error Handler
    RspApiException exception = RspApiException.fromJson(jsonDecode(response.body));
    exception.originalException = null;
    exception.stackTrace = StackTrace.current;
    exception.httpStatusCode = response.statusCode;
    exception.httpHeaders = response.headers;
    throw ApiException(exception: exception);
  } on ApiException {
    rethrow;
  } on Exception catch (e, stack) {
    throw ApiException(
      exception: RspApiException(message: e.toString(), originalException: e, stackTrace: stack),
    );
  }
}

^1 All repositories are Singleton's, that way they are safe to wire any number of times and anywhere in your code.
^2 Ideally you will not need to use the Uri directly, you should use the Repo instead.
^3 All repositories implement isLoginSuccess() & logOut().

© Adhvent Consulting Ltd.

Libraries

pabulo_sdk