Add support for Apple Pay

This commit is contained in:
Hillel Coren 2017-11-27 16:50:06 +02:00
parent 676560c003
commit 53830430aa
15 changed files with 211 additions and 10 deletions

View File

@ -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');

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -308,7 +308,6 @@ class TaskController extends BaseController
}
} else {
$count = $this->taskService->bulk($ids, $action);
if (request()->wantsJson()) {
return response()->json($count);
} else {

View File

@ -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);

View File

@ -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;

View File

@ -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
*/

View File

@ -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
*

View File

@ -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)) {

View File

@ -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) {

View File

@ -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;

View File

@ -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'))

View File

@ -105,7 +105,7 @@
});
});
</script>
@endif
@endif
@stop
@section('content')

View 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>&nbsp;&nbsp;</p>
<center>
{!! Button::normal(strtoupper(trans('texts.cancel')))->large()->asLinkTo($invitation->getLink()) !!}
&nbsp;&nbsp;
<div id="payment-request-button"></div>
</center>
@stop

View File

@ -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');