We will learn how to do paypal integration for flutter app using webview. There are a few steps we need to follow to do it.
1. Paypal developer account
2. Integrate Flutter for backend
3. Flutter front end code
1. Paypal developer account
First you need to go there https://developer.paypal.com/developer/accounts and create an account as a developer. But do remember it has to be a business type account.
Once you create the account you would be able to create a test app. This test and it's secret id and client id we will use in our app.
2. Integerate paypal for backend
Here I am going to use Laravel framework as a backend. You should go to the below link and install paypal PHP sdk using a composer.
https://github.com/paypal/PayPal-PHP-SDK/wiki/Installation-Composer
You should see something like this in your vendor folder in your laravel app
If you see like above photo, you should have the sdk ready inside your laravel app.
Now, this is not going to be restfup api request in from Flutter application. Loading the sdk and getting response from backend will happend using a webview. In our case we are using webview_flutter plugin.
So flutter app will make a request to the backend using web view. Since this is not restful api request, we will put our end points in web.php file
<?php
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return view('welcome');
});
Route::group(['prefix' => 'payment-mobile'], function () {
Route::get('/', 'PaymentController@payment')->name('payment-mobile');
Route::get('set-payment-method/{name}', 'PaymentController@set_payment_method')->name('set-payment-method');
});
Route::post('pay-paypal', 'PaypalPaymentController@payWithpaypal')->name('pay-paypal');
Route::get('paypal-status', 'PaypalPaymentController@getPaymentStatus')->name('paypal-status');
Route::get('payment-success', 'PaymentController@success')->name('payment-success');
Route::get('payment-fail', 'PaymentController@fail')->name('payment-fail');
The above route could be anything based on your requirement. if we use the below link from flutter webview
http://mvs.bslmeiyu.com/payment-mobile?customer_id=27&order_id=100011
then, the second line from the above code in Route::group will get called.
After that we need to create two controllers and one view file. Controllers in would be in the Http/Controllers folder
1. PaymentController.php
2. PaypalPaymentController.php
The view would be in resources/view folder
1. payment-view.blade.php
Now take a look at PaymentController.php
<?php
namespace App\Http\Controllers;
use App\Models\Order;
use App\Models\User;
use Illuminate\Http\Request;
class PaymentController extends Controller
{
public function payment(Request $request)
{
if ($request->has('callback')) {
Order::where(['id' => $request->order_id])->update(['callback' => $request['callback']]);
}
session()->put('customer_id', $request['customer_id']);
session()->put('order_id', $request->order_id);
$customer = User::find($request['customer_id']);
$order = Order::where(['id' => $request->order_id, 'user_id' => $request['customer_id']])->first();
if (isset($customer) && isset($order)) {
$data = [
'name' => $customer['f_name'],
'email' => $customer['email'],
'phone' => $customer['phone'],
];
session()->put('data', $data);
return view('payment-view');
}
return response()->json(['errors' => ['code' => 'order-payment', 'message' => 'Data not found']], 403);
}
public function success()
{
$order = Order::where(['id' => session('order_id'), 'user_id'=>session('customer_id')])->first();
/*if ($order->callback != null) {
return redirect($order->callback . '&status=success');
}
return response()->json(['message' => 'Payment succeeded'], 200); */
return redirect('&status=success');
}
public function fail()
{
$order = Order::where(['id' => session('order_id'), 'user_id'=>session('customer_id')])->first();
/*if ($order->callback != null) {
return redirect($order->callback . '&status=fail');
}
return response()->json(['message' => 'Payment failed'], 403);*/
return redirect('&status=success');
}
}
The above function payment() returns a view to the blade.
Take a look at the view payment-view.blade.php
@php($currency=\App\Models\BusinessSetting::where(['key'=>'currency'])->first()->value)
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<title>
@yield('title')
</title>
<!-- SEO Meta Tags-->
<meta name="description" content="">
<meta name="keywords" content="">
<meta name="author" content="">
<!-- Viewport-->
{{--<meta name="_token" content="{{csrf_token()}}">--}}
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Favicon and Touch Icons-->
<link rel="shortcut icon" href="favicon.ico">
<!-- Font -->
<!-- CSS Implementing Plugins -->
<link rel="stylesheet" href="{{asset('assets/admin')}}/css/vendor.min.css">
<link rel="stylesheet" href="{{asset('assets/admin')}}/vendor/icon-set/style.css">
<link rel="stylesheet" href="{{asset('assets/admin')}}/css/custom.css">
<!-- CSS Front Template -->
<link rel="stylesheet" href="{{asset('assets/admin')}}/css/theme.minc619.css?v=1.0">
<style>
.stripe-button-el {
display: none !important;
}
.razorpay-payment-button {
display: none !important;
}
</style>
<script
src="{{asset('assets/admin')}}/vendor/hs-navbar-vertical-aside/hs-navbar-vertical-aside-mini-cache.js"></script>
<link rel="stylesheet" href="{{asset('assets/admin')}}/css/toastr.css">
</head>
<!-- Body-->
<body class="toolbar-enabled">
<!-- Page Content-->
<div class="container pb-5 mb-2 mb-md-4">
<div class="row">
<div class="col-md-12 mb-5 pt-5">
<center class="">
<h1>Payment method</h1>
</center>
</div>
@php($order=\App\Models\Order::find(session('order_id')))
<section class="col-lg-12">
<div class="checkout_details mt-3">
<div class="row">
@php($config=\App\CentralLogics\Helpers::get_business_settings('paypal'))
@if($config['status'])
<div class="col-md-6 mb-4" style="cursor: pointer">
<div class="card">
<div class="card-body pb-0 pt-1" style="height: 70px">
<form class="needs-validation" method="POST" id="payment-form"
action="{{route('pay-paypal')}}">
{{ csrf_field() }}
<button class="btn btn-block" type="submit">
<img width="100"
src="{{asset('assets/admin/img/paypal.png')}}"/>
</button>
</form>
</div>
</div>
</div>
@endif
</div>
</div>
</section>
</div>
</div>
<!-- JS Front -->
<script src="{{asset('assets/admin')}}/js/custom.js"></script>
<script src="{{asset('assets/admin')}}/js/vendor.min.js"></script>
<script src="{{asset('assets/admin')}}/js/theme.min.js"></script>
<script src="{{asset('assets/admin')}}/js/sweet_alert.js"></script>
<script src="{{asset('assets/admin')}}/js/toastr.js"></script>
<script src="{{asset('assets/admin')}}/js/bootstrap.min.js"></script>
{!! Toastr::message() !!}
</body>
</html>
In the above code, we have used some css and js files for styling. You can use yours.
We also have an image for paypal. You can use any kind of image.
Now take a look at PaypalPaymentController.php
<?php
namespace App\Http\Controllers;
use Brian2694\Toastr\Facades\Toastr;
use App\CentralLogics\Helpers;
use App\CentralLogics\OrderLogic;
use App\Models\Order;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str;
use PayPal\Api\Amount;
use PayPal\Api\Item;
use PayPal\Api\ItemList;
use PayPal\Api\Payer;
use PayPal\Api\Payment;
use PayPal\Api\PaymentExecution;
use PayPal\Api\RedirectUrls;
use PayPal\Api\Transaction;
use PayPal\Auth\OAuthTokenCredential;
use PayPal\Common\PayPalModel;
use PayPal\Rest\ApiContext;
//require __DIR__ . '/PayPal-PHP-SDK/autoload.php';
//require __DIR__ . '/PayPal-PHP-SDK/autoload.php';
class PaypalPaymentController extends Controller
{
public function __construct()
{
$paypal_conf = Config::get('paypal');
$this->_api_context = new \PayPal\Rest\ApiContext(new OAuthTokenCredential(
$paypal_conf['client_id'],
$paypal_conf['secret'])
);
$this->_api_context->setConfig($paypal_conf['settings']);
}
public function payWithpaypal(Request $request)
{
$order = Order::with(['details'])->where(['id' => session('order_id')])->first();
$tr_ref = Str::random(6) . '-' . rand(1, 1000);
$payer = new Payer();
$payer->setPaymentMethod('paypal');
$items_array = [];
$item = new Item();
$number = sprintf("%0.2f", $order['order_amount']);
$item->setName($order->customer['f_name'])
->setCurrency(Helpers::currency_code())
->setQuantity(1)
->setPrice($number);
array_push($items_array, $item);
$item_list = new ItemList();
$item_list->setItems($items_array);
$amount = new Amount();
$amount->setCurrency(Helpers::currency_code())
->setTotal($number);
\session()->put('transaction_reference', $tr_ref);
$transaction = new Transaction();
$transaction->setAmount($amount)
->setItemList($item_list)
->setDescription($tr_ref);
$redirect_urls = new RedirectUrls();
$redirect_urls->setReturnUrl(URL::route('paypal-status'))
->setCancelUrl(URL::route('payment-fail'));
$payment = new Payment();
$payment->setIntent('Sale')
->setPayer($payer)
->setRedirectUrls($redirect_urls)
->setTransactions(array($transaction));
try {
$payment->create($this->_api_context);
/**
* Get redirect url
* The API response provides the url that you must redirect
* the buyer to. Retrieve the url from the $payment->getLinks() method
*
*/
foreach ($payment->getLinks() as $key => $link) {
if ($link->getRel() == 'approval_url') {
$redirectUrl = $link->getHref();
break;
}
}
DB::table('orders')
->where('id', $order->id)
->update([
'transaction_reference' => $payment->getId(),
'payment_method' => 'paypal',
'order_status' => 'success',
'failed' => now(),
'updated_at' => now()
]);
Session::put('paypal_payment_id', $payment->getId());
if (isset($redirectUrl)) {
return Redirect::away($redirectUrl);
}else{
dd("bye");
}
} catch (\Exception $ex) {
dd($ex->getData());
// Toastr::error(trans($ex->getData(),['method'=>trans('messages.paypal')]));
Toastr::error(trans('messages.your_currency_is_not_supported',['method'=>trans('messages.paypal')]));
return back();
}
Session::put('error', trans('messages.config_your_account',['method'=>trans('messages.paypal')]));
return back();
}
public function getPaymentStatus(Request $request)
{
$payment_id = Session::get('paypal_payment_id');
if (empty($request['PayerID']) || empty($request['token'])) {
Session::put('error', trans('messages.payment_failed'));
return Redirect::back();
}
$payment = Payment::get($payment_id, $this->_api_context);
$execution = new PaymentExecution();
$execution->setPayerId($request['PayerID']);
/**Execute the payment **/
$result = $payment->execute($execution, $this->_api_context);
$order = Order::where('transaction_reference', $payment_id)->first();
if ($result->getState() == 'approved') {
$order->transaction_reference = $payment_id;
$order->payment_method = 'paypal';
$order->payment_status = 'paid';
$order->order_status = 'confirmed';
$order->confirmed = now();
$order->save();
/*try {
Helpers::send_order_notification($order);
} catch (\Exception $e) {
} */
return redirect('&status=success');
/*if ($order->callback != null) {
return redirect($order->callback . '&status=success');
}else{
return \redirect()->route('payment-success');
}*/
}
$order->order_status = 'failed';
$order->failed = now();
$order->save();
return redirect('&status=fail');
/*if ($order->callback != null) {
return redirect($order->callback . '&status=fail');
}else{
return \redirect()->route('payment-fail');
}*/
}
}
95% of the above code is from here. It's paypal's official code. You may need to change a little bit based on your requirement.
3. Flutter front end code
Here I am going to share with you flutter front end code for checking. This tutorial assumes that you have an app ready and you also have a check out button.
If you click on your check out button, it should send a request to the server which in return will call paypal sdk
class PaymentScreen extends StatefulWidget {
final OrderModel orderModel;
PaymentScreen({required this.orderModel});
@override
_PaymentScreenState createState() => _PaymentScreenState();
}
class _PaymentScreenState extends State<PaymentScreen> {
late String selectedUrl;
double value = 0.0;
bool _canRedirect = true;
bool _isLoading = true;
final Completer<WebViewController> _controller = Completer<WebViewController>();
late WebViewController controllerGlobal;
@override
void initState() {
super.initState();
selectedUrl = '${AppConstants.BASE_URL}/payment-mobile?customer_id=${widget.orderModel.userId}&order_id=${widget.orderModel.id}';
//selectedUrl="https://mvs.bslmeiyu.com";
// if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () => _exitApp(context),
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text("Payment"),
leading: IconButton(
icon: Icon(Icons.arrow_back_ios),
onPressed:()=> _exitApp(context),
),
backgroundColor: AppColors.mainColor,
),
body: Center(
child: Container(
width: Dimensions.WEB_MAX_WIDTH,
child: Stack(
children: [
WebView(
javascriptMode: JavascriptMode.unrestricted,
initialUrl: selectedUrl,
gestureNavigationEnabled: true,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_3 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13E233 Safari/601.1',
onWebViewCreated: (WebViewController webViewController) {
_controller.future.then((value) => controllerGlobal = value);
_controller.complete(webViewController);
//_controller.future.catchError(onError)
},
onProgress: (int progress) {
print("WebView is loading (progress : $progress%)");
},
onPageStarted: (String url) {
print('Page started loading: $url');
setState(() {
_isLoading = true;
});
print("printing urls "+url.toString());
_redirect(url);
},
onPageFinished: (String url) {
print('Page finished loading: $url');
setState(() {
_isLoading = false;
});
_redirect(url);
},
),
_isLoading ? Center(
child: CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor)),
) : SizedBox.shrink(),
],
),
),
),
),
);
}
void _redirect(String url) {
print("redirect");
if(_canRedirect) {
bool _isSuccess = url.contains('success') && url.contains(AppConstants.BASE_URL);
bool _isFailed = url.contains('fail') && url.contains(AppConstants.BASE_URL);
bool _isCancel = url.contains('cancel') && url.contains(AppConstants.BASE_URL);
if (_isSuccess || _isFailed || _isCancel) {
_canRedirect = false;
}
if (_isSuccess) {
Get.offNamed(RouteHelper.getOrderSuccessRoute(widget.orderModel.id.toString(), 'success'));
} else if (_isFailed || _isCancel) {
Get.offNamed(RouteHelper.getOrderSuccessRoute(widget.orderModel.id.toString(), 'fail'));
}else{
print("Encountered problem");
}
}
}
Future<bool> _exitApp(BuildContext context) async {
if (await controllerGlobal.canGoBack()) {
controllerGlobal.goBack();
return Future.value(false);
} else {
print("app exited");
return true;
// return Get.dialog(PaymentFailedDialog(orderID: widget.orderModel.id.toString()));
}
}
}
Tables needed for this app. We will create three tables
1. business_settings (this table is not needed if you don't follow e-commerce app)
Create a table in phpmyadmin. Look at the tables column. Here we will save configuratoin infor. Name table business_settings
Get the model from the below link
https://laravell.site/Laravel-e-commerce-app-business-settings-model
2. orders table
Here it will keep information for a certain order. Create another table name orders.
We need order model for orders table inside app/Models/. Get the code from the link below.
https://laravell.site/Laravel-e-commerce-app-order-model
3. order_details
This table helps you keep all the orders details in the database. Create it name it order_details
order_details need a model in the app/Models/ folder. Get the model from the link below
https://laravell.site/laravel-e-commerce-app-order-detail-model
Error and solutions
If you get below error
"error":"invalid_client","error_description":"Client Authentication failed"
Then try to get a new paypal client id and secret id and save it to the .evn file and restart your app or web app.
2024-07-30 09:00:54
https://laravell.site/Laravel-e-commerce-app-business-settings-model https://laravell.site/laravel-e-commerce-app-order-detail-model https://laravell.site/Laravel-e-commerce-app-order-model links arent working
2024-07-30 09:00:51
https://laravell.site/Laravel-e-commerce-app-business-settings-model https://laravell.site/laravel-e-commerce-app-order-detail-model https://laravell.site/Laravel-e-commerce-app-order-model links arent working
where did you get the links from?
2024-03-06 02:11:20
It doesn't work now! I have this problem: sizeof(): Argument #1 ($value) must be of type Countable|array, string given. Can you help me ???