mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-07-09 03:14:30 -04:00
Add support for Apple Pay
This commit is contained in:
parent
676560c003
commit
53830430aa
@ -2,6 +2,7 @@
|
||||
|
||||
if (! defined('APP_NAME')) {
|
||||
define('APP_NAME', env('APP_NAME', 'Invoice Ninja'));
|
||||
define('APP_DOMAIN', env('APP_DOMAIN', 'invoiceninja.com'));
|
||||
define('CONTACT_EMAIL', env('MAIL_FROM_ADDRESS', env('MAIL_USERNAME')));
|
||||
define('CONTACT_NAME', env('MAIL_FROM_NAME'));
|
||||
define('SITE_URL', env('APP_URL'));
|
||||
@ -431,6 +432,7 @@ if (! defined('APP_NAME')) {
|
||||
define('GATEWAY_TYPE_SOFORT', 8);
|
||||
define('GATEWAY_TYPE_SEPA', 9);
|
||||
define('GATEWAY_TYPE_GOCARDLESS', 10);
|
||||
define('GATEWAY_TYPE_APPLE_PAY', 11);
|
||||
define('GATEWAY_TYPE_TOKEN', 'token');
|
||||
|
||||
define('TEMPLATE_INVOICE', 'invoice');
|
||||
|
@ -17,6 +17,7 @@ use Utils;
|
||||
use Validator;
|
||||
use View;
|
||||
use WePay;
|
||||
use File;
|
||||
|
||||
class AccountGatewayController extends BaseController
|
||||
{
|
||||
@ -297,6 +298,13 @@ class AccountGatewayController extends BaseController
|
||||
$config->enableSofort = boolval(Input::get('enable_sofort'));
|
||||
$config->enableSepa = boolval(Input::get('enable_sepa'));
|
||||
$config->enableBitcoin = boolval(Input::get('enable_bitcoin'));
|
||||
$config->enableApplePay = boolval(Input::get('enable_apple_pay'));
|
||||
|
||||
if ($config->enableApplePay && $uploadedFile = request()->file('apple_merchant_id')) {
|
||||
$config->appleMerchantId = File::get($uploadedFile);
|
||||
} elseif ($oldConfig && ! empty($oldConfig->appleMerchantId)) {
|
||||
$config->appleMerchantId = $oldConfig->appleMerchantId;
|
||||
}
|
||||
}
|
||||
|
||||
if ($gatewayId == GATEWAY_STRIPE || $gatewayId == GATEWAY_WEPAY) {
|
||||
|
@ -114,10 +114,16 @@ class OnlinePaymentController extends BaseController
|
||||
*
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function doPayment(CreateOnlinePaymentRequest $request)
|
||||
public function doPayment(CreateOnlinePaymentRequest $request, $invitationKey, $gatewayTypeAlias = false)
|
||||
{
|
||||
$invitation = $request->invitation;
|
||||
$gatewayTypeId = Session::get($invitation->id . 'gateway_type');
|
||||
|
||||
if ($gatewayTypeAlias) {
|
||||
$gatewayTypeId = GatewayType::getIdFromAlias($gatewayTypeAlias);
|
||||
} else {
|
||||
$gatewayTypeId = Session::get($invitation->id . 'gateway_type');
|
||||
}
|
||||
|
||||
$paymentDriver = $invitation->account->paymentDriver($invitation, $gatewayTypeId);
|
||||
|
||||
if (! $invitation->invoice->canBePaid() && ! request()->update) {
|
||||
@ -184,7 +190,9 @@ class OnlinePaymentController extends BaseController
|
||||
|
||||
private function completePurchase($invitation, $isOffsite = false)
|
||||
{
|
||||
if ($redirectUrl = session('redirect_url:' . $invitation->invitation_key)) {
|
||||
if (request()->wantsJson()) {
|
||||
return response()->json(RESULT_SUCCESS);
|
||||
} elseif ($redirectUrl = session('redirect_url:' . $invitation->invitation_key)) {
|
||||
$separator = strpos($redirectUrl, '?') === false ? '?' : '&';
|
||||
|
||||
return redirect()->to($redirectUrl . $separator . 'invoice_id=' . $invitation->invoice->public_id);
|
||||
@ -412,4 +420,28 @@ class OnlinePaymentController extends BaseController
|
||||
return redirect()->to($link);
|
||||
}
|
||||
}
|
||||
|
||||
public function showAppleMerchantId()
|
||||
{
|
||||
if (Utils::isNinja()) {
|
||||
$subdomain = Utils::getSubdomain(\Request::server('HTTP_HOST'));
|
||||
$account = Account::whereSubdomain($subdomain)->first();
|
||||
} else {
|
||||
$account = Account::first();
|
||||
}
|
||||
|
||||
if (! $account) {
|
||||
exit("Account not found");
|
||||
}
|
||||
|
||||
$accountGateway = $account->account_gateways()
|
||||
->whereGatewayId(GATEWAY_STRIPE)->first();
|
||||
|
||||
if (! $account) {
|
||||
exit("Apple merchant id not set");
|
||||
}
|
||||
|
||||
echo $accountGateway->getConfigField('appleMerchantId');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
@ -308,7 +308,6 @@ class TaskController extends BaseController
|
||||
}
|
||||
} else {
|
||||
$count = $this->taskService->bulk($ids, $action);
|
||||
|
||||
if (request()->wantsJson()) {
|
||||
return response()->json($count);
|
||||
} else {
|
||||
|
@ -3,6 +3,7 @@
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Models\Invitation;
|
||||
use App\Models\GatewayType;
|
||||
|
||||
class CreateOnlinePaymentRequest extends Request
|
||||
{
|
||||
@ -26,7 +27,7 @@ class CreateOnlinePaymentRequest extends Request
|
||||
$account = $this->invitation->account;
|
||||
|
||||
$paymentDriver = $account->paymentDriver($this->invitation, $this->gateway_type);
|
||||
|
||||
|
||||
return $paymentDriver->rules();
|
||||
}
|
||||
|
||||
@ -39,7 +40,12 @@ class CreateOnlinePaymentRequest extends Request
|
||||
->firstOrFail();
|
||||
|
||||
$input['invitation'] = $invitation;
|
||||
$input['gateway_type'] = session($invitation->id . 'gateway_type');
|
||||
|
||||
if ($gatewayTypeAlias = request()->gateway_type) {
|
||||
$input['gateway_type'] = GatewayType::getIdFromAlias($gatewayTypeAlias);
|
||||
} else {
|
||||
$input['gateway_type'] = session($invitation->id . 'gateway_type');
|
||||
}
|
||||
|
||||
$this->replace($input);
|
||||
|
||||
|
@ -108,6 +108,11 @@ class Utils
|
||||
return self::getResllerType() ? true : false;
|
||||
}
|
||||
|
||||
public static function isRootFolder()
|
||||
{
|
||||
return strlen(preg_replace('/[^\/]/', '', url('/'))) == 2;
|
||||
}
|
||||
|
||||
public static function clientViewCSS()
|
||||
{
|
||||
$account = false;
|
||||
|
@ -136,6 +136,15 @@ class AccountGateway extends EntityModel
|
||||
return $this->getConfigField('publishableKey');
|
||||
}
|
||||
|
||||
public function getAppleMerchantId()
|
||||
{
|
||||
if (! $this->isGateway(GATEWAY_STRIPE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getConfigField('appleMerchantId');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
@ -144,6 +153,14 @@ class AccountGateway extends EntityModel
|
||||
return ! empty($this->getConfigField('enableAch'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function getApplePayEnabled()
|
||||
{
|
||||
return ! empty($this->getConfigField('enableApplePay'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
@ -502,6 +502,20 @@ class Client extends EntityModel
|
||||
return $this->account->currency ? $this->account->currency->code : 'USD';
|
||||
}
|
||||
|
||||
public function getCountryCode()
|
||||
{
|
||||
if ($country = $this->country) {
|
||||
return $country->iso_3166_2;
|
||||
}
|
||||
|
||||
if (! $this->account) {
|
||||
$this->load('account');
|
||||
}
|
||||
|
||||
return $this->account->country ? $this->account->country->iso_3166_2 : 'US';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param $isQuote
|
||||
*
|
||||
|
@ -53,6 +53,9 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
if ($gateway->getAlipayEnabled()) {
|
||||
$types[] = GATEWAY_TYPE_ALIPAY;
|
||||
}
|
||||
if ($gateway->getApplePayEnabled()) {
|
||||
$types[] = GATEWAY_TYPE_APPLE_PAY;
|
||||
}
|
||||
}
|
||||
|
||||
return $types;
|
||||
@ -67,6 +70,10 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
{
|
||||
$rules = parent::rules();
|
||||
|
||||
if ($this->isGatewayType(GATEWAY_TYPE_APPLE_PAY)) {
|
||||
return ['sourceToken' => 'required'];
|
||||
}
|
||||
|
||||
if ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {
|
||||
$rules['authorize_ach'] = 'required';
|
||||
}
|
||||
@ -224,7 +231,9 @@ class StripePaymentDriver extends BasePaymentDriver
|
||||
|
||||
// For older users the Stripe account may just have the customer token but not the card version
|
||||
// In that case we'd use GATEWAY_TYPE_TOKEN even though we're creating the credit card
|
||||
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD) || $this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
|
||||
if ($this->isGatewayType(GATEWAY_TYPE_CREDIT_CARD)
|
||||
|| $this->isGatewayType(GATEWAY_TYPE_APPLE_PAY)
|
||||
|| $this->isGatewayType(GATEWAY_TYPE_TOKEN)) {
|
||||
$paymentMethod->expiration = $source['exp_year'] . '-' . $source['exp_month'] . '-01';
|
||||
$paymentMethod->payment_type_id = PaymentType::parseCardType($source['brand']);
|
||||
} elseif ($this->isGatewayType(GATEWAY_TYPE_BANK_TRANSFER)) {
|
||||
|
@ -19,6 +19,7 @@ class GatewayTypesSeeder extends Seeder
|
||||
['alias' => 'sofort', 'name' => 'Sofort'],
|
||||
['alias' => 'sepa', 'name' => 'SEPA'],
|
||||
['alias' => 'gocardless', 'name' => 'GoCardless'],
|
||||
['alias' => 'apple_pay', 'name' => 'Apple Pay'],
|
||||
];
|
||||
|
||||
foreach ($gateway_types as $gateway_type) {
|
||||
|
@ -2558,6 +2558,15 @@ $LANG = array(
|
||||
'scheduled_report_attached' => 'Your scheduled :type report is attached.',
|
||||
'scheduled_report_error' => 'Failed to create schedule report',
|
||||
'invalid_one_time_password' => 'Invalid one time password',
|
||||
'apple_pay' => 'Apple/Google Pay',
|
||||
'enable_apple_pay' => 'Accept Apple Pay and Pay with Google',
|
||||
'requires_subdomain' => 'This payment type requires that a :link.',
|
||||
'subdomain_is_set' => 'subdomain is set',
|
||||
'verification_file' => 'Verification File',
|
||||
'verification_file_missing' => 'The verification file is needed to accept payments.',
|
||||
'apple_pay_domain' => 'Use <code>:domain</code> as the domain in :link.',
|
||||
'apple_pay_not_supported' => 'Sorry, Apple/Google Pay isn\'t supported',
|
||||
|
||||
);
|
||||
|
||||
return $LANG;
|
||||
|
@ -32,7 +32,9 @@
|
||||
<h3 class="panel-title">{!! trans($title) !!}</h3>
|
||||
</div>
|
||||
<div class="panel-body form-padding-right">
|
||||
{!! Former::open($url)->method($method)->rule()->addClass('warn-on-exit') !!}
|
||||
{!! Former::open_for_files($url)
|
||||
->method($method)
|
||||
->addClass('warn-on-exit') !!}
|
||||
|
||||
@if ($accountGateway)
|
||||
{!! Former::populateField('primary_gateway_id', $accountGateway->gateway_id) !!}
|
||||
@ -42,6 +44,7 @@
|
||||
{!! Former::populateField('update_address', intval($accountGateway->update_address)) !!}
|
||||
{!! Former::populateField('publishable_key', $accountGateway->getPublishableStripeKey() ? str_repeat('*', strlen($accountGateway->getPublishableStripeKey())) : '') !!}
|
||||
{!! Former::populateField('enable_ach', $accountGateway->getAchEnabled() ? 1 : 0) !!}
|
||||
{!! Former::populateField('enable_apple_pay', $accountGateway->getApplePayEnabled() ? 1 : 0) !!}
|
||||
{!! Former::populateField('enable_sofort', $accountGateway->getSofortEnabled() ? 1 : 0) !!}
|
||||
{!! Former::populateField('enable_alipay', $accountGateway->getAlipayEnabled() ? 1 : 0) !!}
|
||||
{!! Former::populateField('enable_paypal', $accountGateway->getPayPalEnabled() ? 1 : 0) !!}
|
||||
@ -188,6 +191,23 @@
|
||||
->text(trans('texts.enable_ach'))
|
||||
->value(1) !!}
|
||||
|
||||
{!! Former::checkbox('enable_apple_pay')
|
||||
->label(trans('texts.apple_pay'))
|
||||
->text(trans('texts.enable_apple_pay'))
|
||||
->disabled(Utils::isNinja() && ! $account->subdomain)
|
||||
->help((Utils::isNinja() && ! $account->subdomain) ? trans('texts.requires_subdomain', [
|
||||
'link' => link_to('/settings/client_portal', trans('texts.subdomain_is_set'), ['target' => '_blank'])
|
||||
]) : ($accountGateway->getApplePayEnabled() && Utils::isRootFolder() && ! $accountGateway->getAppleMerchantId() ? 'verification_file_missing' :
|
||||
Utils::isNinja() ? trans('texts.apple_pay_domain', [
|
||||
'domain' => $account->subdomain . '.' . APP_DOMAIN, 'link' => link_to('https://dashboard.stripe.com/account/apple_pay', 'Stripe', ['target' => '_blank']),
|
||||
]) : ''))
|
||||
->value(1) !!}
|
||||
|
||||
@if (Utils::isRootFolder())
|
||||
{!! Former::file('apple_merchant_id')
|
||||
->label('verification_file') !!}
|
||||
@endif
|
||||
|
||||
{!! Former::checkbox('enable_sofort')
|
||||
->label(trans('texts.sofort'))
|
||||
->text(trans('texts.enable_sofort'))
|
||||
|
@ -105,7 +105,7 @@
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endif
|
||||
@endif
|
||||
@stop
|
||||
|
||||
@section('content')
|
||||
|
78
resources/views/payments/apple_pay.blade.php
Normal file
78
resources/views/payments/apple_pay.blade.php
Normal file
@ -0,0 +1,78 @@
|
||||
@extends('payments.payment_method')
|
||||
|
||||
@section('head')
|
||||
@parent
|
||||
|
||||
<script type="text/javascript" src="https://js.stripe.com/v3/"></script>
|
||||
<script type="text/javascript">
|
||||
// https://stripe.com/docs/stripe-js/elements/payment-request-button
|
||||
var stripe = Stripe('{{ $accountGateway->getPublishableStripeKey() }}');
|
||||
var paymentRequest = stripe.paymentRequest({
|
||||
country: '{{ $client->getCountryCode() }}',
|
||||
currency: '{{ strtolower($client->getCurrencyCode()) }}',
|
||||
total: {
|
||||
label: '{{ trans('texts.invoice') . ' ' . $invitation->invoice->invoice_number }}',
|
||||
amount: {{ $invitation->invoice->getRequestedAmount() * 100 }},
|
||||
},
|
||||
});
|
||||
|
||||
var elements = stripe.elements();
|
||||
var prButton = elements.create('paymentRequestButton', {
|
||||
paymentRequest: paymentRequest,
|
||||
});
|
||||
|
||||
$(function() {
|
||||
// Check the availability of the Payment Request API first.
|
||||
paymentRequest.canMakePayment().then(function(result) {
|
||||
if (result) {
|
||||
prButton.mount('#payment-request-button');
|
||||
} else {
|
||||
document.getElementById('payment-request-button').style.display = 'none';
|
||||
document.getElementById('error-message').style.display = 'inline';
|
||||
}
|
||||
});
|
||||
|
||||
paymentRequest.on('token', function(ev) {
|
||||
$.ajax({
|
||||
dataType: 'json',
|
||||
type: 'post',
|
||||
data: {sourceToken: ev.token.id},
|
||||
url: '{{ $invitation->getLink('payment') }}/apple_pay',
|
||||
accepts: {
|
||||
json: 'application/json'
|
||||
},
|
||||
success: function(response) {
|
||||
if (response == '{{ RESULT_SUCCESS }}') {
|
||||
ev.complete('success');
|
||||
location.reload();
|
||||
} else {
|
||||
ev.complete('fail');
|
||||
}
|
||||
},
|
||||
error: function(error) {
|
||||
ev.complete('fail');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@stop
|
||||
|
||||
@section('payment_details')
|
||||
|
||||
<center>
|
||||
<div id="error-message" style="display:none">{{ trans('texts.apple_pay_not_supported') }}</div>
|
||||
</center>
|
||||
|
||||
<p> </p>
|
||||
|
||||
<center>
|
||||
{!! Button::normal(strtoupper(trans('texts.cancel')))->large()->asLinkTo($invitation->getLink()) !!}
|
||||
|
||||
<div id="payment-request-button"></div>
|
||||
</center>
|
||||
|
||||
@stop
|
@ -21,7 +21,7 @@ Route::group(['middleware' => ['lookup:contact', 'auth:client']], function () {
|
||||
Route::get('view', 'HomeController@viewLogo');
|
||||
Route::get('approve/{invitation_key}', 'QuoteController@approve');
|
||||
Route::get('payment/{invitation_key}/{gateway_type?}/{source_id?}', 'OnlinePaymentController@showPayment');
|
||||
Route::post('payment/{invitation_key}', 'OnlinePaymentController@doPayment');
|
||||
Route::post('payment/{invitation_key}/{gateway_type?}', 'OnlinePaymentController@doPayment');
|
||||
Route::get('complete_source/{invitation_key}/{gateway_type}', 'OnlinePaymentController@completeSource');
|
||||
Route::match(['GET', 'POST'], 'complete/{invitation_key?}/{gateway_type?}', 'OnlinePaymentController@offsitePayment');
|
||||
Route::get('bank/{routing_number}', 'OnlinePaymentController@getBankInfo');
|
||||
@ -72,6 +72,7 @@ Route::group(['middleware' => 'lookup:account'], function () {
|
||||
Route::match(['GET', 'POST', 'OPTIONS'], '/buy_now/{gateway_type?}', 'OnlinePaymentController@handleBuyNow');
|
||||
Route::get('validate_two_factor/{account_key}', 'Auth\LoginController@getValidateToken');
|
||||
Route::post('validate_two_factor/{account_key}', ['middleware' => 'throttle:5', 'uses' => 'Auth\LoginController@postValidateToken']);
|
||||
Route::get('.well-known/apple-developer-merchantid-domain-association', 'OnlinePaymentController@showAppleMerchantId');
|
||||
});
|
||||
|
||||
//Route::post('/hook/bot/{platform?}', 'BotController@handleMessage');
|
||||
|
Loading…
x
Reference in New Issue
Block a user