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:
baseUrl: this is the path to the backend where the core platform is running , i.e.https://api.domain.comorhttps://api.domain.com:8080requestTimeOut: Optional The time asDurationsto wait for backend to respond. If ignored the default is 30 seconds.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'.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:
- Request Objects: Objects that are used as part of the
POST,PUT,PATCHoperations to send data to the backend begin with a prefixReqand these classes are under the pathsrc/models/request. - Response Objects: Objects that receive the data as part of the
response.bodybegin with a prefixRspand these classes are under the pathsrc/models/response. - Data Transfer Objects (DTO): are objects that hold the actual data, they are part
of the
RequestandResponseobjects, these classes are suffixed withDtoand implementvalid()andcopyWith()functionality or additional helper functions to simplify the use of these objects, and these classes are under the pathsrc/models/factory. - 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
Repoand are under the pathsrc/repositories. - Uri: Universal Resource Indicator are classes with
staticmethods that help you call the backend directly if there is a need to bypass the SDK repo operations, these classes begin with a prefixUriand follow by the Swagger grouping name. e.g.UriConsumerwill have all the end-points related to consumer interactions.{^2} Under theUriclasses there are two types ofstaticdefinitions:- uri => statics starting with prefix uri are end-points that do not have
RequestParamin the uri. - url => statics starting with prefix url are end-points that will need
additional
ReqestParamto call the backend.
- uri => statics starting with prefix uri are end-points that do not have
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.
- Show a screen to user to interact with.
- Collect the data in a
Request. - Call the respective repository function with the request.
- Receive a
Responseback 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:
RepoAdmin.letMeIn()create or update a JWT token, also the 🏃🏽➡️ START of the JWT lifecycle management.isLoginSuccess(){^3} check if there is a valid JWT Token, iftruecontinue, iffalseshow the login screen, get the user credentials and callRepoAdmin.letMeIn().logOut(){^3} explicitly remove the cached JWT token. 🚫 This is final, once the JWT is removed from the cache, you will need to issueletMeIn().
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
message: to hold the message.timeOfEvent: the date and time this event occurred.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.
- Errors that are caught by underline conditions or operations and
rethrownasApiExceptionor newthrow ApiException(). - Errors that are converted from the
response.statusfrom the backend. - 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 & 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.