diff --git a/app/Constants.php b/app/Constants.php index 36c480065839..dea9f2936848 100644 --- a/app/Constants.php +++ b/app/Constants.php @@ -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'); diff --git a/app/Http/Controllers/AccountGatewayController.php b/app/Http/Controllers/AccountGatewayController.php index e4b907d33dbe..559840caaa2b 100644 --- a/app/Http/Controllers/AccountGatewayController.php +++ b/app/Http/Controllers/AccountGatewayController.php @@ -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) { diff --git a/app/Http/Controllers/OnlinePaymentController.php b/app/Http/Controllers/OnlinePaymentController.php index 4a490add165e..852f0f7bc1e6 100644 --- a/app/Http/Controllers/OnlinePaymentController.php +++ b/app/Http/Controllers/OnlinePaymentController.php @@ -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; + } } diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index 4091667352e4..2ca566196da6 100644 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -308,7 +308,6 @@ class TaskController extends BaseController } } else { $count = $this->taskService->bulk($ids, $action); - if (request()->wantsJson()) { return response()->json($count); } else { diff --git a/app/Http/Requests/CreateOnlinePaymentRequest.php b/app/Http/Requests/CreateOnlinePaymentRequest.php index 1e9910082b9d..387f25c0f84c 100644 --- a/app/Http/Requests/CreateOnlinePaymentRequest.php +++ b/app/Http/Requests/CreateOnlinePaymentRequest.php @@ -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); diff --git a/app/Libraries/Utils.php b/app/Libraries/Utils.php index 10173e56ab78..afab900eee0c 100644 --- a/app/Libraries/Utils.php +++ b/app/Libraries/Utils.php @@ -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; diff --git a/app/Models/AccountGateway.php b/app/Models/AccountGateway.php index 5e78f4aca62c..a5757ad7448e 100644 --- a/app/Models/AccountGateway.php +++ b/app/Models/AccountGateway.php @@ -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 */ diff --git a/app/Models/Client.php b/app/Models/Client.php index 10ef1a1510c9..44c43e378c01 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -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 * diff --git a/app/Ninja/PaymentDrivers/StripePaymentDriver.php b/app/Ninja/PaymentDrivers/StripePaymentDriver.php index 6024b0d0c72f..ae0622238327 100644 --- a/app/Ninja/PaymentDrivers/StripePaymentDriver.php +++ b/app/Ninja/PaymentDrivers/StripePaymentDriver.php @@ -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)) { diff --git a/database/seeds/GatewayTypesSeeder.php b/database/seeds/GatewayTypesSeeder.php index c7a3075a4cfa..7e4bbf6cb961 100644 --- a/database/seeds/GatewayTypesSeeder.php +++ b/database/seeds/GatewayTypesSeeder.php @@ -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) { diff --git a/resources/lang/en/texts.php b/resources/lang/en/texts.php index 5534a3965ea2..44f0fccf1b77 100644 --- a/resources/lang/en/texts.php +++ b/resources/lang/en/texts.php @@ -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 :domain as the domain in :link.', + 'apple_pay_not_supported' => 'Sorry, Apple/Google Pay isn\'t supported', + ); return $LANG; diff --git a/resources/views/accounts/account_gateway.blade.php b/resources/views/accounts/account_gateway.blade.php index 2d1b570c849d..76cdded0d343 100644 --- a/resources/views/accounts/account_gateway.blade.php +++ b/resources/views/accounts/account_gateway.blade.php @@ -32,7 +32,9 @@

{!! trans($title) !!}

- {!! 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')) diff --git a/resources/views/invoices/view.blade.php b/resources/views/invoices/view.blade.php index 252ea9185740..08791072175f 100644 --- a/resources/views/invoices/view.blade.php +++ b/resources/views/invoices/view.blade.php @@ -105,7 +105,7 @@ }); }); - @endif + @endif @stop @section('content') diff --git a/resources/views/payments/apple_pay.blade.php b/resources/views/payments/apple_pay.blade.php new file mode 100644 index 000000000000..dd24b9a6bc2e --- /dev/null +++ b/resources/views/payments/apple_pay.blade.php @@ -0,0 +1,78 @@ +@extends('payments.payment_method') + +@section('head') + @parent + + + + +@stop + +@section('payment_details') + +
+ +
+ +

  

+ +
+ {!! Button::normal(strtoupper(trans('texts.cancel')))->large()->asLinkTo($invitation->getLink()) !!} +    +
+
+ +@stop diff --git a/routes/web.php b/routes/web.php index 9d0f39e9a150..83c11fe47564 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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');