mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-03 13:04:38 -04:00
display occured errors in flow with a view
This commit is contained in:
parent
166a370f80
commit
3735845c44
@ -47,7 +47,7 @@ class Nordigen
|
|||||||
public function getInstitutions()
|
public function getInstitutions()
|
||||||
{
|
{
|
||||||
if ($this->test_mode)
|
if ($this->test_mode)
|
||||||
return (array) $this->client->institution->getInstitution($this->sandbox_institutionId);
|
return [$this->client->institution->getInstitution($this->sandbox_institutionId)];
|
||||||
|
|
||||||
return $this->client->institution->getInstitutions();
|
return $this->client->institution->getInstitutions();
|
||||||
}
|
}
|
||||||
|
@ -32,31 +32,50 @@ class NordigenController extends BaseController
|
|||||||
$data = $request->all();
|
$data = $request->all();
|
||||||
$context = $request->getTokenContent();
|
$context = $request->getTokenContent();
|
||||||
|
|
||||||
if (!$context || $context["context"] != "nordigen" || array_key_exists("requisitionId", $context))
|
if (!$context)
|
||||||
return response()->redirectTo(($context && array_key_exists("redirect", $context) ? $context["redirect"] : config('ninja.app_url')) . "?action=nordigen_connect&status=failed&reason=token-invalid");
|
return view('bank.nordigen.handler', [
|
||||||
|
'failed_reason' => "token-invalid",
|
||||||
|
"redirectUrl" => config("ninja.app_url") . "?action=nordigen_connect&status=failed&reason=token-invalid",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$context["redirect"] = $data["redirect"];
|
||||||
|
if ($context["context"] != "nordigen" || array_key_exists("requisitionId", $context))
|
||||||
|
return view('bank.nordigen.handler', [
|
||||||
|
'failed_reason' => "token-invalid",
|
||||||
|
"redirectUrl" => ($context["redirect"]) . "?action=nordigen_connect&status=failed&reason=token-invalid",
|
||||||
|
]);
|
||||||
|
|
||||||
$company = $request->getCompany();
|
$company = $request->getCompany();
|
||||||
$account = $company->account;
|
$account = $company->account;
|
||||||
|
|
||||||
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
||||||
return response()->redirectTo($data["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid");
|
return view('bank.nordigen.handler', [
|
||||||
|
'company' => $company,
|
||||||
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "account-config-invalid",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid",
|
||||||
|
]);
|
||||||
|
|
||||||
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise')))
|
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise')))
|
||||||
return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=not-available");
|
return view('bank.nordigen.handler', [
|
||||||
|
'company' => $company,
|
||||||
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "not-available",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=not-available",
|
||||||
|
]);
|
||||||
|
|
||||||
$nordigen = new Nordigen();
|
$nordigen = new Nordigen();
|
||||||
|
|
||||||
// show bank_selection_screen, when institution_id is not present
|
// show bank_selection_screen, when institution_id is not present
|
||||||
if (!array_key_exists("institution_id", $data)) {
|
if (!array_key_exists("institution_id", $data)) {
|
||||||
$data = [
|
$data = [
|
||||||
'token' => $request->token,
|
|
||||||
'context' => $context,
|
|
||||||
'institutions' => $nordigen->getInstitutions(),
|
|
||||||
'company' => $company,
|
'company' => $company,
|
||||||
'account' => $company->account,
|
'account' => $company->account,
|
||||||
|
'institutions' => $nordigen->getInstitutions(),
|
||||||
|
'redirectUrl' => $context["redirect"] . "?action=nordigen_connect&status=user-aborted"
|
||||||
];
|
];
|
||||||
|
|
||||||
return view('bank.nordigen.connect', $data);
|
return view('bank.nordigen.handler', $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect to requisition flow
|
// redirect to requisition flow
|
||||||
@ -64,20 +83,33 @@ class NordigenController extends BaseController
|
|||||||
$requisition = $nordigen->createRequisition(config('ninja.app_url') . '/nordigen/confirm', $data['institution_id'], $request->token);
|
$requisition = $nordigen->createRequisition(config('ninja.app_url') . '/nordigen/confirm', $data['institution_id'], $request->token);
|
||||||
} catch (NordigenException $e) { // TODO: property_exists returns null in these cases... => why => therefore we just get unknown error everytime $responseBody is typeof GuzzleHttp\Psr7\Stream
|
} catch (NordigenException $e) { // TODO: property_exists returns null in these cases... => why => therefore we just get unknown error everytime $responseBody is typeof GuzzleHttp\Psr7\Stream
|
||||||
Log::error($e);
|
Log::error($e);
|
||||||
$responseBody = $e->getResponse()->getBody();
|
Log::info((string) $e->getResponse()->getBody());
|
||||||
Log::info($responseBody);
|
$responseBody = (string) $e->getResponse()->getBody();
|
||||||
|
|
||||||
if (property_exists($responseBody, "institution_id")) // provided institution_id was wrong
|
if (str_contains($responseBody, '"institution_id"')) // provided institution_id was wrong
|
||||||
return response()->redirectTo($data["redirect"] . "?action=nordigen_connect&status=failed&reason=institution-invalid");
|
return view('bank.nordigen.handler', [
|
||||||
else if (property_exists($responseBody, "reference")) // this error can occur, when a reference was used double or is invalid => therefor we suggest the frontend to use another token
|
'company' => $company,
|
||||||
return response()->redirectTo($data["redirect"] . "?action=nordigen_connect&status=failed&reason=token-invalid");
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "institution-invalid",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=institution-invalid",
|
||||||
|
]);
|
||||||
|
else if (str_contains($responseBody, '"reference"')) // this error can occur, when a reference was used double or is invalid => therefor we suggest the frontend to use another token
|
||||||
|
return view('bank.nordigen.handler', [
|
||||||
|
'company' => $company,
|
||||||
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "token-invalid",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=token-invalid",
|
||||||
|
]);
|
||||||
else
|
else
|
||||||
return response()->redirectTo($data["redirect"] . "?action=nordigen_connect&status=failed&reason=unknown");
|
return view('bank.nordigen.handler', [
|
||||||
|
'company' => $company,
|
||||||
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "unknown",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=unknown",
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// save cache
|
// save cache
|
||||||
if (array_key_exists("redirect", $data))
|
|
||||||
$context["redirect"] = $data["redirect"];
|
|
||||||
$context["requisitionId"] = $requisition["id"];
|
$context["requisitionId"] = $requisition["id"];
|
||||||
Cache::put($request->token, $context, 3600);
|
Cache::put($request->token, $context, 3600);
|
||||||
|
|
||||||
@ -155,17 +187,30 @@ class NordigenController extends BaseController
|
|||||||
|
|
||||||
$context = Cache::get($data["ref"]);
|
$context = Cache::get($data["ref"]);
|
||||||
if (!$context || $context["context"] != "nordigen" || !array_key_exists("requisitionId", $context))
|
if (!$context || $context["context"] != "nordigen" || !array_key_exists("requisitionId", $context))
|
||||||
return response()->redirectTo(($context && array_key_exists("redirect", $context) ? $context["redirect"] : config('ninja.app_url')) . "?action=nordigen_connect&status=failed&reason=ref-invalid");
|
return view('bank.nordigen.handler', [
|
||||||
|
'failed_reason' => "ref-invalid",
|
||||||
|
"redirectUrl" => ($context && array_key_exists("redirect", $context) ? $context["redirect"] : config('ninja.app_url')) . "?action=nordigen_connect&status=failed&reason=ref-invalid",
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
$company = Company::where('company_key', $context["company_key"])->firstOrFail();
|
$company = Company::where('company_key', $context["company_key"])->firstOrFail();
|
||||||
$account = $company->account;
|
$account = $company->account;
|
||||||
|
|
||||||
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
if (!(config('ninja.nordigen.secret_id') && config('ninja.nordigen.secret_key')))
|
||||||
return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid");
|
return view('bank.nordigen.handler', [
|
||||||
|
'company' => $company,
|
||||||
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "account-config-invalid",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=account-config-invalid",
|
||||||
|
]);
|
||||||
|
|
||||||
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise')))
|
if (!(Ninja::isSelfHost() || (Ninja::isHosted() && $account->isPaid() && $account->plan == 'enterprise')))
|
||||||
return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=not-available");
|
return view('bank.nordigen.handler', [
|
||||||
|
'company' => $company,
|
||||||
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "not-available",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=not-available",
|
||||||
|
]);
|
||||||
|
|
||||||
// fetch requisition
|
// fetch requisition
|
||||||
$nordigen = new Nordigen();
|
$nordigen = new Nordigen();
|
||||||
@ -173,11 +218,26 @@ class NordigenController extends BaseController
|
|||||||
|
|
||||||
// check validity of requisition
|
// check validity of requisition
|
||||||
if (!$requisition)
|
if (!$requisition)
|
||||||
return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-not-found");
|
return view('bank.nordigen.handler', [
|
||||||
|
'company' => $company,
|
||||||
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "requisition-not-found",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-not-found",
|
||||||
|
]);
|
||||||
if ($requisition["status"] != "LN")
|
if ($requisition["status"] != "LN")
|
||||||
return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-invalid-status&status=" . $requisition["status"]);
|
return view('bank.nordigen.handler', [
|
||||||
|
'company' => $company,
|
||||||
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "requisition-invalid-status",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-invalid-status&status=" . $requisition["status"],
|
||||||
|
]);
|
||||||
if (sizeof($requisition["accounts"]) == 0)
|
if (sizeof($requisition["accounts"]) == 0)
|
||||||
return response()->redirectTo($context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-no-accounts");
|
return view('bank.nordigen.handler', [
|
||||||
|
'company' => $company,
|
||||||
|
'account' => $company->account,
|
||||||
|
'failed_reason' => "requisition-no-accounts",
|
||||||
|
"redirectUrl" => $context["redirect"] . "?action=nordigen_connect&status=failed&reason=requisition-no-accounts",
|
||||||
|
]);
|
||||||
|
|
||||||
// connect new accounts
|
// connect new accounts
|
||||||
$bank_integration_ids = [];
|
$bank_integration_ids = [];
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
@extends('layouts.ninja')
|
|
||||||
@section('meta_title', ctrans('texts.new_bank_account'))
|
|
||||||
|
|
||||||
@push('head')
|
|
||||||
|
|
||||||
<link href="https://unpkg.com/nordigen-bank-ui@1.5.2/package/src/selector.min.css" rel="stylesheet" />
|
|
||||||
|
|
||||||
@endpush
|
|
||||||
|
|
||||||
@section('body')
|
|
||||||
|
|
||||||
<div id="institution-content-wrapper"></div>
|
|
||||||
|
|
||||||
@endsection
|
|
||||||
|
|
||||||
@push('footer')
|
|
||||||
|
|
||||||
<script type='text/javascript' src='https://unpkg.com/nordigen-bank-ui@1.5.2/package/src/selector.min.js'></script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
|
|
||||||
// Pass your redirect link after user has been authorized in institution
|
|
||||||
const config = {
|
|
||||||
// Text that will be displayed on the left side under the logo. Text is limited to 100 characters, and rest will be truncated. @turbo124 replace with a translated version like ctrans()
|
|
||||||
text: "{{ $account && !$account->isPaid() ? 'Invoice Ninja' : (isset($company) && !is_null($company) ? $company->name : '') }} will gain access for your selected bank account. After selecting your institution you are redirected to theire front-page to complete the request with your account credentials.",
|
|
||||||
// Logo URL that will be shown below the modal form.
|
|
||||||
logoUrl: "{{ $account && !$account->isPaid() ? asset('images/invoiceninja-black-logo-2.png') : (isset($company) && !is_null($company) ? $company->present()->logo() : '') }}",
|
|
||||||
// Will display country list with corresponding institutions. When `countryFilter` is set to `false`, only list of institutions will be shown.
|
|
||||||
countryFilter: false,
|
|
||||||
// style configs
|
|
||||||
styles: {
|
|
||||||
// Primary
|
|
||||||
// Link to google font
|
|
||||||
fontFamily: new URL("assets/fonts/Roboto-Regular.ttf", window.location.origin).href,
|
|
||||||
fontSize: '15',
|
|
||||||
backgroundColor: '#F2F2F2',
|
|
||||||
textColor: '#222',
|
|
||||||
headingColor: '#222',
|
|
||||||
linkColor: '#8d9090',
|
|
||||||
// Modal
|
|
||||||
modalTextColor: '#1B2021',
|
|
||||||
modalBackgroundColor: '#fff',
|
|
||||||
// Button
|
|
||||||
buttonColor: '#3A53EE',
|
|
||||||
buttonTextColor: '#fff'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
new institutionSelector(@json($institutions), 'institution-modal-content', config);
|
|
||||||
|
|
||||||
const institutionList = Array.from(document.querySelectorAll('.ob-list-institution > a'));
|
|
||||||
|
|
||||||
institutionList.forEach((institution) => {
|
|
||||||
institution.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
const institutionId = institution.getAttribute('data-institution');
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
url.searchParams.set('institution_id', institutionId);
|
|
||||||
window.location.href = url.href;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
@endpush
|
|
138
resources/views/bank/nordigen/handler.blade.php
Normal file
138
resources/views/bank/nordigen/handler.blade.php
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
@extends('layouts.ninja')
|
||||||
|
@section('meta_title', ctrans('texts.new_bank_account'))
|
||||||
|
|
||||||
|
@push('head')
|
||||||
|
|
||||||
|
<link href="https://unpkg.com/nordigen-bank-ui@1.5.2/package/src/selector.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
|
||||||
|
<div id="institution-content-wrapper"></div>
|
||||||
|
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('footer')
|
||||||
|
|
||||||
|
<script type='text/javascript' src='https://unpkg.com/nordigen-bank-ui@1.5.2/package/src/selector.min.js'></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
|
||||||
|
// Pass your redirect link after user has been authorized in institution
|
||||||
|
const config = {
|
||||||
|
// Redirect URL that is being used when modal is being closed.
|
||||||
|
redirectUrl: "{{ $redirectUrl }}" || new URL("", window.location.origin).href,
|
||||||
|
// Text that will be displayed on the left side under the logo. Text is limited to 100 characters, and rest will be truncated. @turbo124 replace with a translated version like ctrans()
|
||||||
|
text: "{{ ($account ?? false) && !$account->isPaid() ? 'Invoice Ninja' : (isset($company) && !is_null($company) ? $company->name : 'Invoice Ninja') }} will gain access for your selected bank account. After selecting your institution you are redirected to theire front-page to complete the request with your account credentials.",
|
||||||
|
// Logo URL that will be shown below the modal form.
|
||||||
|
logoUrl: "{{ ($account ?? false) && !$account->isPaid() ? asset('images/invoiceninja-black-logo-2.png') : (isset($company) && !is_null($company) ? $company->present()->logo() : asset('images/invoiceninja-black-logo-2.png')) }}",
|
||||||
|
// Will display country list with corresponding institutions. When `countryFilter` is set to `false`, only list of institutions will be shown.
|
||||||
|
countryFilter: false,
|
||||||
|
// style configs
|
||||||
|
styles: {
|
||||||
|
// Primary
|
||||||
|
// Link to google font
|
||||||
|
fontFamily: new URL("assets/fonts/Roboto-Regular.ttf", window.location.origin).href,
|
||||||
|
fontSize: '15',
|
||||||
|
backgroundColor: '#F2F2F2',
|
||||||
|
textColor: '#222',
|
||||||
|
headingColor: '#222',
|
||||||
|
linkColor: '#8d9090',
|
||||||
|
// Modal
|
||||||
|
modalTextColor: '#1B2021',
|
||||||
|
modalBackgroundColor: '#fff',
|
||||||
|
// Button
|
||||||
|
buttonColor: '#3A53EE',
|
||||||
|
buttonTextColor: '#fff'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const failedReason = "{{ $failed_reason ?? '' }}".trim();
|
||||||
|
|
||||||
|
new institutionSelector(@json($institutions ?? []), 'institution-modal-content', config);
|
||||||
|
|
||||||
|
if (!failedReason) {
|
||||||
|
|
||||||
|
const institutionList = Array.from(document.querySelectorAll('.ob-list-institution > a'));
|
||||||
|
|
||||||
|
institutionList.forEach((institution) => {
|
||||||
|
institution.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const institutionId = institution.getAttribute('data-institution');
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.searchParams.set('institution_id', institutionId);
|
||||||
|
window.location.href = url.href;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.getElementsByClassName("institution-search-container")[0].remove();
|
||||||
|
document.getElementsByClassName("institution-container")[0].remove();
|
||||||
|
|
||||||
|
const heading = document.querySelectorAll('h2')[0];
|
||||||
|
const wrapper = document.getElementById("institution-modal-content");
|
||||||
|
const contents = document.createElement("div");
|
||||||
|
contents.id = "failed-container";
|
||||||
|
contents.className = "mt-2";
|
||||||
|
contents.style["font-size"] = "80%";
|
||||||
|
contents.style["opacity"] = "80%";
|
||||||
|
|
||||||
|
let restartFlow = false; // return, restart, refresh
|
||||||
|
heading.innerHTML = "An Error has occured";
|
||||||
|
contents.innerHTML = "An unknown Error has occured! Reason: " + failedReason;
|
||||||
|
switch (failedReason) {
|
||||||
|
// Connect Screen Errors
|
||||||
|
case "token-invalid":
|
||||||
|
heading.innerHTML = "Invalid Token";
|
||||||
|
contents.innerHTML = "The provided token was invalid. Please restart the flow, with a valid one_time_token. Contact support for help, if this issue persists.";
|
||||||
|
break;
|
||||||
|
case "account-config-invalid":
|
||||||
|
heading.innerHTML = "Missing Credentials";
|
||||||
|
contents.innerHTML = "The provided credentials for nordigen are eighter missing or invalid. Contact support for help, if this issue persists.";
|
||||||
|
break;
|
||||||
|
case "not-available":
|
||||||
|
heading.innerHTML = "Not Available";
|
||||||
|
contents.innerHTML = "This flow is not available for your account. Considder upgrading to enterprise version. Contact support for help, if this issue persists.";
|
||||||
|
break;
|
||||||
|
case "institution-invalid":
|
||||||
|
restartFlow = true;
|
||||||
|
heading.innerHTML = "Invalid Institution";
|
||||||
|
contents.innerHTML = "The provided institution-id is invalid or no longer valid. You can go to the bank selection page by clicking the button below or cancel the flow by clicking on the 'X' above.";
|
||||||
|
break;
|
||||||
|
// Confirm Screen Errors
|
||||||
|
case "ref-invalid":
|
||||||
|
heading.innerHTML = "Invalid Reference";
|
||||||
|
contents.innerHTML = "Nordigen did not provide a valid reference. Please run flow again and contact support, if this issue persists.";
|
||||||
|
break;
|
||||||
|
case "requisition-not-found":
|
||||||
|
heading.innerHTML = "Invalid Requisition";
|
||||||
|
contents.innerHTML = "Nordigen did not provide a valid reference. Please run flow again and contact support, if this issue persists.";
|
||||||
|
break;
|
||||||
|
case "requisition-invalid-status":
|
||||||
|
heading.innerHTML = "Not Ready";
|
||||||
|
contents.innerHTML = "You may called this site to early. Please finish authorization and refresh this page. Contact support for help, if this issue persists.";
|
||||||
|
break;
|
||||||
|
case "requisition-no-accounts":
|
||||||
|
heading.innerHTML = "No Accounts selected";
|
||||||
|
contents.innerHTML = "The service has not returned any valid accounts. Considder restarting the flow.";
|
||||||
|
break;
|
||||||
|
case "unknown":
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn('Invalid or missing failed_reason code: ' + failedReason);
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
wrapper.appendChild(contents);
|
||||||
|
|
||||||
|
const restartUrl = new URL(window.location.pathname, window.location.origin); // no searchParams
|
||||||
|
const returnButton = document.createElement('div');
|
||||||
|
returnButton.className = "mt-4";
|
||||||
|
returnButton.innerHTML = `<a class="button button-primary bg-blue-600 my-4" href="${restartFlow ? restartUrl.href : config.redirectUrl}">${restartFlow ? 'Restart flow.' : 'Return to application.'}</a>`
|
||||||
|
wrapper.appendChild(returnButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@endpush
|
Loading…
x
Reference in New Issue
Block a user