Added elements for mobile payments. BOD-90
This commit is contained in:
@@ -24,6 +24,9 @@ const Unauthorized = lazy(() =>
|
|||||||
import("../pages/unauthorized/unauthorized.component")
|
import("../pages/unauthorized/unauthorized.component")
|
||||||
);
|
);
|
||||||
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
const CsiPage = lazy(() => import("../pages/csi/csi.container.page"));
|
||||||
|
const MobilePaymentContainer = lazy(() =>
|
||||||
|
import("../pages/mobile-payment/mobile-payment.container")
|
||||||
|
);
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
currentUser: selectCurrentUser,
|
currentUser: selectCurrentUser,
|
||||||
@@ -50,20 +53,25 @@ export function App({ checkUserSession, currentUser }) {
|
|||||||
<div>
|
<div>
|
||||||
<Switch>
|
<Switch>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<Suspense fallback={<LoadingSpinner message='App.Js Suspense' />}>
|
<Suspense fallback={<LoadingSpinner message="App.Js Suspense" />}>
|
||||||
<Route exact path='/' component={LandingPage} />
|
<Route exact path="/" component={LandingPage} />
|
||||||
<Route exact path='/unauthorized' component={Unauthorized} />
|
<Route exact path="/unauthorized" component={Unauthorized} />
|
||||||
<Route exact path='/signin' component={SignInPage} />
|
<Route exact path="/signin" component={SignInPage} />
|
||||||
<Route exact path='/resetpassword' component={ResetPassword} />
|
<Route exact path="/resetpassword" component={ResetPassword} />
|
||||||
<Route exact path='/csi/:surveyId' component={CsiPage} />
|
<Route exact path="/csi/:surveyId" component={CsiPage} />
|
||||||
|
<Route
|
||||||
|
exact
|
||||||
|
path="/mp/:paymentIs"
|
||||||
|
component={MobilePaymentContainer}
|
||||||
|
/>
|
||||||
<PrivateRoute
|
<PrivateRoute
|
||||||
isAuthorized={currentUser.authorized}
|
isAuthorized={currentUser.authorized}
|
||||||
path='/manage'
|
path="/manage"
|
||||||
component={ManagePage}
|
component={ManagePage}
|
||||||
/>
|
/>
|
||||||
<PrivateRoute
|
<PrivateRoute
|
||||||
isAuthorized={currentUser.authorized}
|
isAuthorized={currentUser.authorized}
|
||||||
path='/tech'
|
path="/tech"
|
||||||
component={TechPageContainer}
|
component={TechPageContainer}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import React from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { createStructuredSelector } from "reselect";
|
import { createStructuredSelector } from "reselect";
|
||||||
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
import { selectBodyshop } from "../../redux/user/user.selectors";
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
import { setEmailOptions } from "../../redux/email/email.actions";
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
import { TemplateList } from "../../utils/TemplateConstants";
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
import {
|
||||||
|
PaymentRequestButtonElement,
|
||||||
|
useStripe,
|
||||||
|
Elements,
|
||||||
|
useElements,
|
||||||
|
} from "@stripe/react-stripe-js";
|
||||||
|
|
||||||
const mapStateToProps = createStructuredSelector({
|
const mapStateToProps = createStructuredSelector({
|
||||||
bodyshop: selectBodyshop,
|
bodyshop: selectBodyshop,
|
||||||
@@ -15,86 +21,53 @@ const mapDispatchToProps = (dispatch) => ({
|
|||||||
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
setEmailOptions: (e) => dispatch(setEmailOptions(e)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
function Test({ bodyshop, setEmailOptions }) {
|
||||||
mapStateToProps,
|
const stripe = useStripe();
|
||||||
mapDispatchToProps
|
|
||||||
)(function Test({ bodyshop, setEmailOptions }) {
|
const [paymentRequest, setPaymentRequest] = useState(null);
|
||||||
const handle = async () => {
|
useEffect(() => {
|
||||||
const response = await axios.post(
|
if (stripe) {
|
||||||
"/accounting/iif/receivables",
|
console.log("in useeff");
|
||||||
{ jobId: "661dd1d5-bf06-426f-8bd2-bd9e41de8eb1" },
|
const pr = stripe.paymentRequest({
|
||||||
{
|
country: "CA",
|
||||||
headers: {
|
displayItems: [{ label: "Deductible", amount: 1099 }],
|
||||||
Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`,
|
currency: "cad",
|
||||||
|
total: {
|
||||||
|
label: "Demo total",
|
||||||
|
amount: 1099,
|
||||||
},
|
},
|
||||||
}
|
requestPayerName: true,
|
||||||
);
|
requestPayerEmail: true,
|
||||||
console.log("handle -> result", response);
|
});
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.href = url;
|
|
||||||
link.setAttribute(
|
|
||||||
"download",
|
|
||||||
response.headers.filename || "receivables.iif"
|
|
||||||
); //or any other extension
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLocal = async () => {
|
console.log("pr", pr);
|
||||||
try {
|
// Check the availability of the Payment Request API.
|
||||||
const response = await axios.post(
|
pr.canMakePayment().then((result) => {
|
||||||
"http://localhost:1337/qb/receivables",
|
console.log("result", result);
|
||||||
{ jobId: "661dd1d5-bf06-426f-8bd2-bd9e41de8eb1" },
|
if (result) {
|
||||||
{
|
setPaymentRequest(pr);
|
||||||
headers: {
|
} else {
|
||||||
Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`,
|
var details = {
|
||||||
},
|
total: { label: "", amount: { currency: "CAD", value: "0.00" } },
|
||||||
|
};
|
||||||
|
// new PaymentRequest(
|
||||||
|
// [{ supportedMethods: ["basic-card"] }],
|
||||||
|
// {}
|
||||||
|
// // details
|
||||||
|
// ).show();
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
console.log("handle -> result", response);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("error", JSON.stringify(error));
|
|
||||||
}
|
}
|
||||||
};
|
}, [stripe]);
|
||||||
|
|
||||||
const handleQbxml = async () => {
|
if (paymentRequest) {
|
||||||
const response = await axios.post(
|
console.log("****************render");
|
||||||
"/accounting/qbxml/receivables",
|
return (
|
||||||
{ jobId: "661dd1d5-bf06-426f-8bd2-bd9e41de8eb1" },
|
<div style={{ height: "300px" }}>
|
||||||
{
|
<PaymentRequestButtonElement options={{ paymentRequest }} />
|
||||||
headers: {
|
</div>
|
||||||
Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
console.log("handle -> XML", response);
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const response2 = await axios.post(
|
|
||||||
"http://localhost:1337/qb/receivables",
|
|
||||||
response.data,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${await auth.currentUser.getIdToken(true)}`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
console.log("handle -> result", response2);
|
|
||||||
} catch (error) {
|
|
||||||
console.log("error", error, JSON.stringify(error));
|
|
||||||
}
|
|
||||||
|
|
||||||
// const url = window.URL.createObjectURL(new Blob([response.data]));
|
|
||||||
// const link = document.createElement("a");
|
|
||||||
// link.href = url;
|
|
||||||
// link.setAttribute(
|
|
||||||
// "download",
|
|
||||||
// response.headers.filename || "receivables.iif"
|
|
||||||
// ); //or any other extension
|
|
||||||
// document.body.appendChild(link);
|
|
||||||
// link.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -116,9 +89,6 @@ export default connect(
|
|||||||
>
|
>
|
||||||
send email
|
send email
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handle}>Hit with Header.</button>
|
|
||||||
<button onClick={handleLocal}>Hit Localhost with Header.</button>
|
|
||||||
<button onClick={handleQbxml}>Qbxml</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
|
logImEXEvent("IMEXEVENT", { somethignArThare: 5 });
|
||||||
@@ -128,4 +98,6 @@ export default connect(
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(Test);
|
||||||
|
|||||||
@@ -61,54 +61,56 @@ export const logImEXEvent = (eventName, additionalParams, stateProp = null) => {
|
|||||||
analytics.logEvent(eventName, eventParams);
|
analytics.logEvent(eventName, eventParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
messaging.onMessage(async (payload) => {
|
if (messaging) {
|
||||||
console.log("**********UTILS Message received. ", payload);
|
messaging.onMessage(async (payload) => {
|
||||||
navigator.serviceWorker.getRegistration().then((registration) => {
|
console.log("**********UTILS Message received. ", payload);
|
||||||
return registration.showNotification(
|
navigator.serviceWorker.getRegistration().then((registration) => {
|
||||||
"[UTIL]" + payload.notification.title,
|
return registration.showNotification(
|
||||||
payload.notification
|
"[UTIL]" + payload.notification.title,
|
||||||
);
|
payload.notification
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// if (!payload.clientId) return;
|
||||||
|
|
||||||
|
// // Get the client.
|
||||||
|
// const client = await clients.get(payload.clientId);
|
||||||
|
// // Exit early if we don't get the client.
|
||||||
|
// // Eg, if it closed.
|
||||||
|
// if (!client) return;
|
||||||
|
|
||||||
|
// // Send a message to the client.
|
||||||
|
// console.log("Posting to client.");
|
||||||
|
// client.postMessage({
|
||||||
|
// msg: "Hey I just got a fetch from you!",
|
||||||
|
// url: payload.request.url,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// [START_EXCLUDE]
|
||||||
|
// Update the UI to include the received message.
|
||||||
|
//appendMessage(payload);
|
||||||
|
|
||||||
|
// [END_EXCLUDE]
|
||||||
});
|
});
|
||||||
|
|
||||||
// if (!payload.clientId) return;
|
messaging.onTokenRefresh(() => {
|
||||||
|
messaging
|
||||||
// // Get the client.
|
.getToken()
|
||||||
// const client = await clients.get(payload.clientId);
|
.then((refreshedToken) => {
|
||||||
// // Exit early if we don't get the client.
|
console.log("**********Token refreshed.");
|
||||||
// // Eg, if it closed.
|
// Indicate that the new Instance ID token has not yet been sent to the
|
||||||
// if (!client) return;
|
// app server.
|
||||||
|
// setTokenSentToServer(false);
|
||||||
// // Send a message to the client.
|
// // Send Instance ID token to app server.
|
||||||
// console.log("Posting to client.");
|
// sendTokenToServer(refreshedToken);
|
||||||
// client.postMessage({
|
// // [START_EXCLUDE]
|
||||||
// msg: "Hey I just got a fetch from you!",
|
// // Display new Instance ID token and clear UI of all previous messages.
|
||||||
// url: payload.request.url,
|
// resetUI();
|
||||||
// });
|
// [END_EXCLUDE]
|
||||||
|
})
|
||||||
// [START_EXCLUDE]
|
.catch((err) => {
|
||||||
// Update the UI to include the received message.
|
console.log("**********Unable to retrieve refreshed token ", err);
|
||||||
//appendMessage(payload);
|
// showToken("Unable to retrieve refreshed token ", err);
|
||||||
|
});
|
||||||
// [END_EXCLUDE]
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
messaging.onTokenRefresh(() => {
|
|
||||||
messaging
|
|
||||||
.getToken()
|
|
||||||
.then((refreshedToken) => {
|
|
||||||
console.log("**********Token refreshed.");
|
|
||||||
// Indicate that the new Instance ID token has not yet been sent to the
|
|
||||||
// app server.
|
|
||||||
// setTokenSentToServer(false);
|
|
||||||
// // Send Instance ID token to app server.
|
|
||||||
// sendTokenToServer(refreshedToken);
|
|
||||||
// // [START_EXCLUDE]
|
|
||||||
// // Display new Instance ID token and clear UI of all previous messages.
|
|
||||||
// resetUI();
|
|
||||||
// [END_EXCLUDE]
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.log("**********Unable to retrieve refreshed token ", err);
|
|
||||||
// showToken("Unable to retrieve refreshed token ", err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -165,15 +165,20 @@ export function Manage({ match, conflict }) {
|
|||||||
<LoadingSpinner message={t("general.labels.loadingapp")} />
|
<LoadingSpinner message={t("general.labels.loadingapp")} />
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<Elements stripe={stripePromise}>
|
||||||
|
<PaymentModalContainer />
|
||||||
|
</Elements>
|
||||||
<BreadCrumbs />
|
<BreadCrumbs />
|
||||||
<EnterInvoiceModalContainer />
|
<EnterInvoiceModalContainer />
|
||||||
<JobCostingModal />
|
<JobCostingModal />
|
||||||
<EmailOverlayContainer />
|
<EmailOverlayContainer />
|
||||||
<TimeTicketModalContainer />
|
<TimeTicketModalContainer />
|
||||||
<PrintCenterModalContainer />
|
<PrintCenterModalContainer />
|
||||||
<Elements stripe={stripePromise}>
|
<Route
|
||||||
<PaymentModalContainer />
|
exact
|
||||||
</Elements>
|
path={`${match.path}/_test`}
|
||||||
|
component={TestComponent}
|
||||||
|
/>
|
||||||
<Route exact path={`${match.path}`} component={ManageRootPage} />
|
<Route exact path={`${match.path}`} component={ManageRootPage} />
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
@@ -338,7 +343,6 @@ export function Manage({ match, conflict }) {
|
|||||||
path={`${match.path}/scoreboard`}
|
path={`${match.path}/scoreboard`}
|
||||||
component={Scoreboard}
|
component={Scoreboard}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
exact
|
exact
|
||||||
path={`${match.path}/timetickets`}
|
path={`${match.path}/timetickets`}
|
||||||
|
|||||||
93
client/src/pages/mobile-payment/mobile-payment.component.jsx
Normal file
93
client/src/pages/mobile-payment/mobile-payment.component.jsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { connect } from "react-redux";
|
||||||
|
import { createStructuredSelector } from "reselect";
|
||||||
|
import { auth, logImEXEvent } from "../../firebase/firebase.utils";
|
||||||
|
import { selectBodyshop } from "../../redux/user/user.selectors";
|
||||||
|
import { setEmailOptions } from "../../redux/email/email.actions";
|
||||||
|
import { TemplateList } from "../../utils/TemplateConstants";
|
||||||
|
import {
|
||||||
|
PaymentRequestButtonElement,
|
||||||
|
useStripe,
|
||||||
|
Elements,
|
||||||
|
useElements,
|
||||||
|
} from "@stripe/react-stripe-js";
|
||||||
|
|
||||||
|
export default function MobilePaymentComponent() {
|
||||||
|
const stripe = useStripe();
|
||||||
|
|
||||||
|
const [paymentRequest, setPaymentRequest] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (stripe) {
|
||||||
|
console.log("in useeff");
|
||||||
|
const pr = stripe.paymentRequest({
|
||||||
|
country: "CA",
|
||||||
|
displayItems: [{ label: "Deductible", amount: 1 }],
|
||||||
|
currency: "cad",
|
||||||
|
total: {
|
||||||
|
label: "Demo total",
|
||||||
|
amount: 1,
|
||||||
|
},
|
||||||
|
requestPayerName: true,
|
||||||
|
requestPayerEmail: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("pr", pr);
|
||||||
|
// Check the availability of the Payment Request API.
|
||||||
|
pr.canMakePayment().then((result) => {
|
||||||
|
console.log("result", result);
|
||||||
|
if (result) {
|
||||||
|
setPaymentRequest(pr);
|
||||||
|
} else {
|
||||||
|
var details = {
|
||||||
|
total: { label: "", amount: { currency: "CAD", value: "0.00" } },
|
||||||
|
};
|
||||||
|
// new PaymentRequest(
|
||||||
|
// [{ supportedMethods: ["basic-card"] }],
|
||||||
|
// {}
|
||||||
|
// // details
|
||||||
|
// ).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [stripe]);
|
||||||
|
|
||||||
|
if (paymentRequest) {
|
||||||
|
// paymentRequest.on("paymentmethod", async (ev) => {
|
||||||
|
// //Call server side to get the client secret
|
||||||
|
// // Confirm the PaymentIntent without handling potential next actions (yet).
|
||||||
|
// const { error: confirmError } = await stripe.confirmCardPayment(
|
||||||
|
// clientSecret,
|
||||||
|
// { payment_method: ev.paymentMethod.id },
|
||||||
|
// { handleActions: false }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (confirmError) {
|
||||||
|
// // Report to the browser that the payment failed, prompting it to
|
||||||
|
// // re-show the payment interface, or show an error message and close
|
||||||
|
// // the payment interface.
|
||||||
|
// ev.complete("fail");
|
||||||
|
// } else {
|
||||||
|
// // Report to the browser that the confirmation was successful, prompting
|
||||||
|
// // it to close the browser payment method collection interface.
|
||||||
|
// ev.complete("success");
|
||||||
|
// // Let Stripe.js handle the rest of the payment flow.
|
||||||
|
// const { error, paymentIntent } = await stripe.confirmCardPayment(
|
||||||
|
// clientSecret
|
||||||
|
// );
|
||||||
|
// if (error) {
|
||||||
|
// // The payment failed -- ask your customer for a new payment method.
|
||||||
|
// } else {
|
||||||
|
// // The payment has succeeded.
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
return (
|
||||||
|
<div style={{ height: "300px" }}>
|
||||||
|
<PaymentRequestButtonElement options={{ paymentRequest }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div></div>;
|
||||||
|
}
|
||||||
23
client/src/pages/mobile-payment/mobile-payment.container.jsx
Normal file
23
client/src/pages/mobile-payment/mobile-payment.container.jsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import React from "react";
|
||||||
|
import MobilePaymentComponent from "./mobile-payment.component";
|
||||||
|
import { Elements } from "@stripe/react-stripe-js";
|
||||||
|
import { loadStripe } from "@stripe/stripe-js";
|
||||||
|
|
||||||
|
const stripePromise = new Promise((resolve, reject) => {
|
||||||
|
resolve(
|
||||||
|
loadStripe(process.env.REACT_APP_STRIPE_PUBLIC_KEY, {
|
||||||
|
stripeAccount: "acct_1Fa7lFIEahEZW8b4",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function MobilePaymentContainer() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
The mobile payment container.
|
||||||
|
<Elements stripe={stripePromise}>
|
||||||
|
<MobilePaymentComponent />
|
||||||
|
</Elements>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -83,6 +83,7 @@ app.post("/notifications/send", fb.sendNotification);
|
|||||||
//Stripe Processing
|
//Stripe Processing
|
||||||
var stripe = require("./server/stripe/payment");
|
var stripe = require("./server/stripe/payment");
|
||||||
app.post("/stripe/payment", stripe.payment);
|
app.post("/stripe/payment", stripe.payment);
|
||||||
|
app.post("/stripe/mobilepayment", stripe.mobile_payment);
|
||||||
|
|
||||||
//Tech Console
|
//Tech Console
|
||||||
var tech = require("./server/tech/tech");
|
var tech = require("./server/tech/tech");
|
||||||
|
|||||||
@@ -44,3 +44,38 @@ exports.payment = async (req, res) => {
|
|||||||
res.status(400).send(error);
|
res.status(400).send(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.mobile_payment = async (req, res) => {
|
||||||
|
const { amount, stripe_acct_id } = req.body;
|
||||||
|
console.log("exports.payment -> amount", amount);
|
||||||
|
console.log("exports.payment -> stripe_acct_id", stripe_acct_id);
|
||||||
|
try {
|
||||||
|
await stripe.paymentIntents
|
||||||
|
.create(
|
||||||
|
{
|
||||||
|
//Pull the amounts from the payment request.
|
||||||
|
payment_method_types: ["card"],
|
||||||
|
amount: amount,
|
||||||
|
currency: "cad",
|
||||||
|
application_fee_amount: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stripeAccount: stripe_acct_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(function (paymentIntent) {
|
||||||
|
try {
|
||||||
|
return res.send({
|
||||||
|
clientSecret: paymentIntent.client_secret,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).send({
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log("error", error);
|
||||||
|
res.status(400).send(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user