Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cred customer purchasing #12868

Merged
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
230cce9
checkout works. Work on product selector
abhigyanghosh30 May 11, 2023
8541b4b
writes product info to localstorage
abhigyanghosh30 May 11, 2023
7fc2a3b
read from local if set in product selector
abhigyanghosh30 May 11, 2023
af744b6
remove unused stuff
abhigyanghosh30 May 11, 2023
43ec9b2
Redirect works
abhigyanghosh30 May 12, 2023
9d079b4
thank you page added along with bugfixes
abhigyanghosh30 May 12, 2023
3b6ac90
fixed failing redirect on checkout
abhigyanghosh30 May 19, 2023
5d55ac1
change marketplace for activation keys
abhigyanghosh30 May 19, 2023
9b1b78d
Removed duplicate code
abhigyanghosh30 May 23, 2023
70f6ee2
changes requested by Morgan and Adri
abhigyanghosh30 May 23, 2023
0bd2bee
prettier now
abhigyanghosh30 May 24, 2023
fab4a5a
fix linting issue and remove console logs
abhigyanghosh30 May 25, 2023
40ff457
updating the product selector for the new flow
abhigyanghosh30 May 31, 2023
b0fea5e
product selector almost done. Pending UX
abhigyanghosh30 Jun 1, 2023
353532a
purchasing exam works
abhigyanghosh30 Jun 16, 2023
1013468
added links to buy and edited purchase conf page
abhigyanghosh30 Jun 19, 2023
753dde8
Apply review suggestions
mtruj013 May 26, 2023
8c26b7c
trying to fix exam selector on mobile
abhigyanghosh30 Jun 22, 2023
a54c9d9
fixed spacing issues on exam shop
abhigyanghosh30 Jun 22, 2023
e847a86
added buy button conditional
abhigyanghosh30 Jun 23, 2023
aeabc57
fix scss issues
abhigyanghosh30 Jul 6, 2023
92fd02d
added links to TOS and Data Privacy
abhigyanghosh30 Jul 12, 2023
65c01e2
just change the labels
abhigyanghosh30 Jul 12, 2023
e700e86
Merge branch 'main' into cred-customer-purchasing
abhigyanghosh30 Jul 20, 2023
7f7179a
fix errors with Webhook views
abhigyanghosh30 Jul 21, 2023
401b26e
fixed lint-ts and prettier
abhigyanghosh30 Jul 24, 2023
7e5edda
remove unnecessary print
abhigyanghosh30 Jul 24, 2023
60168cd
change product type headings
abhigyanghosh30 Jul 25, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions static/js/src/advantage/credentials/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import { Integrations } from "@sentry/tracing";
import { ReactQueryDevtools } from "react-query/devtools";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import CredManage from "./components/CredManage";
import CredShop from "./components/CredShop";
import CredKeyShop from "./components/CredKeyShop";
import CredPurchaseConfirmation from "./components/CredPurchaseConfirmation/CredPurchaseConfirmation";
import CredExamShop from "./components/CredExamShop/CredExamShop";
import CredWebhookResponses from "./components/CredWebhookResponses";

const oneHour = 1000 * 60 * 60;
Expand Down Expand Up @@ -38,8 +40,13 @@ function App() {
<QueryClientProvider client={queryClient}>
<Router basename="/credentials/shop">
<Routes>
<Route path="/" element={<CredShop />} />
<Route path="/" element={<CredExamShop />} />
<Route path="/keys" element={<CredKeyShop />} />
<Route path="/manage" element={<CredManage />} />
<Route
path="/order-thank-you"
element={<CredPurchaseConfirmation />}
/>
<Route
path="/webhook_responses"
element={<CredWebhookResponses />}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import React, { useState } from "react";
import {
Button,
Col,
RadioInput,
Row,
Strip,
} from "@canonical/react-components";
import classNames from "classnames";
import { currencyFormatter } from "advantage/react/utils";

const CredExamShop = () => {
const ExamProducts = [
{
id: "cue-linux-essentials",
longId: "lAK5jL8zvMjZOwaysIMQyGRAdOLgTQQH0xpezu2oYp74",
name: "CUE Linux Essentials",
period: "none",
price: {
currency: "USD",
value: 4900,
},
productID: "cue-linux-essentials",
status: "active",
private: false,
marketplace: "canonical-cube",
metadata: [
{
key: "description",
value:
"Prove your basic operational knowledge of Linux by demonstrating your ability to secure, operate and maintain basic system resources. Topics include user and group management, file and filesystem navigation, and logs and installation tasks related to system maintenance.",
},
],
},
{
id: "cue-02-desktop",
longId: "lAMGrt4buzUR0-faJqg-Ot6dgNLn7ubIpWiyDgOrsDCg",
name: "CUE.02 Desktop QuickCert",
price: { value: 4900, currency: "USD" },
productID: "cue-02-desktop",
canBeTrialled: false,
private: true,
marketplace: "canonical-cube",
metadata: [
{
key: "description",
value:
"Demonstrate your knowledge of Ubuntu Desktop administrative essentials. Topics include package management, system installation, data gathering, and managing printing and displays.",
},
],
},
{
id: "cue-03-server",
longId: "lAMGrt4buzUR0-faJqg-Ot6dgNLn7ubIpWiyDgOrsDCg",
name: "CUE.03 Server QuickCert",
price: { value: 4900, currency: "USD" },
productID: "cue-03-server",
canBeTrialled: false,
private: true,
marketplace: "canonical-cube",
metadata: [
{
key: "description",
value:
"Illustrate your knowledge of common Ubuntu Server administrative tasks and troubleshooting. Topics include job control, performance tuning, services management, and Bash scripting.",
},
],
},
];
const [exam, setExam] = useState(0);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setExam(parseInt(event.target.value));
localStorage.setItem("exam-selector", JSON.stringify(event?.target.value));
};
const handleSubmit = (
event:
| React.FormEvent<HTMLFormElement>
| React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault();
localStorage.setItem(
"shop-checkout-data",
JSON.stringify({
product: ExamProducts[exam],
quantity: 1,
action: "purchase",
})
);
location.href = "/account/checkout";
};
return (
<>
<Strip className="product-selector">
<Row>
<h2>Select an exam to purchase</h2>
</Row>
<Row className="u-hide--small">
{ExamProducts.map((examElement, examIndex) => {
return (
<Col size={4} key={examIndex}>
<div
className={classNames({
"p-card--radio--column": true,
"is-selected": exam == examIndex,
})}
>
<label className="p-radio">
<input
className="p-radio__input"
autoComplete="off"
type="radio"
aria-labelledby={examElement.id + "-label"}
value={examIndex}
checked={exam == examIndex}
onChange={handleChange}
disabled={examElement.private}
/>
<span className="p-radio__label">
<RadioInput
labelClassName="inner-label"
label={examElement.name}
checked={exam == examIndex}
value={examIndex}
onChange={handleChange}
disabled={examElement.private}
/>
<hr />
<p style={{ paddingLeft: "1rem", paddingRight: "1rem" }}>
{examElement.metadata[0].value}
</p>
<hr />
<h5
className="u-align--right"
style={{ paddingRight: "1rem" }}
>
{examElement.private
? "Coming Soon!"
: "Price: " +
currencyFormatter.format(
examElement.price.value / 100
)}
</h5>
</span>
</label>
</div>
</Col>
);
})}
</Row>
{ExamProducts.map((examElement, examIndex) => {
return (
<Col className="u-hide u-show--small" size={12} key={examIndex}>
<div
className={classNames({
"p-card--radio--column": true,
"is-selected": exam == examIndex,
})}
onChange={handleChange}
onClick={() => {
setExam(examIndex);
}}
>
<RadioInput
inline
label={examElement.name}
checked={exam == examIndex}
value={examIndex}
onChange={handleChange}
disabled={examElement.private}
/>
<span>
<p style={{ paddingLeft: "1rem", paddingRight: "1rem" }}>
{examElement.metadata[0].value}
</p>
</span>
<span>
<h5
className="u-align--right"
style={{ paddingRight: "1rem" }}
>
{examElement.private
? "Coming Soon!"
: "Price: " +
currencyFormatter.format(examElement.price.value / 100)}
</h5>
</span>
</div>
</Col>
);
})}
</Strip>
<section className="p-strip--light is-shallow p-shop-cart p-shop-cart--cue">
<Row className="u-sv3">
<Col size={6} className="p-text--small-caps">
Your Order
</Col>
</Row>
<Row>
<Col size={6} style={{ display: "flex" }}>
<p className="p-heading--2" style={{ marginBlock: "auto" }}>
{ExamProducts[exam].name}
</p>
</Col>
<Col size={3} small={2} style={{ display: "flex" }}>
<p className="p-heading--2" style={{ marginBlock: "auto" }}>
{currencyFormatter.format(
(ExamProducts[exam]?.price.value ?? 0) / 100 ?? 0
)}
</p>
</Col>
<Col
size={3}
small={2}
style={{ display: "flex" }}
className="u-align--right"
>
<Button
appearance="positive"
onClick={handleSubmit}
style={{ marginBlock: "auto" }}
>
Buy Now
</Button>
</Col>
</Row>
</section>
</>
);
};
export default CredExamShop;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./CredExamShop";
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import React, { useState } from "react";
import {
Button,
Col,
Form,
Input,
Row,
Strip,
} from "@canonical/react-components";
import { currencyFormatter } from "advantage/react/utils";

const CredKeyShop = () => {
const CUEExamKey = {
id: "cue-activation-key",
longId: "lAMGrt4buzUR0-faJqg-Ot6dgNLn7ubIpWiyDgOrsDCg",
name: "CUE Activation Key",
price: { value: 4900, currency: "USD" },
productID: "cue-activation-key",
canBeTrialled: false,
private: false,
marketplace: "canonical-cube",
};
const checkoutData = localStorage.getItem("shop-checkout-data") || "{}";
const parsedCheckoutData = JSON.parse(checkoutData);
const initQuantity: number = parsedCheckoutData?.quantity;
const [quantity, setQuantity] = useState(initQuantity ?? 1);
const handleChange: React.ChangeEventHandler = (
event: React.ChangeEvent<HTMLInputElement>
) => {
event.preventDefault();
setQuantity(parseInt(event.target.value));
};
const handleSubmit = (
event:
| React.FormEvent<HTMLFormElement>
| React.MouseEvent<HTMLButtonElement>
) => {
event.preventDefault();
localStorage.setItem(
"shop-checkout-data",
JSON.stringify({
product: CUEExamKey,
quantity: quantity,
action: "purchase",
})
);
location.href = "/account/checkout";
};
return (
<>
<Strip>
<Row>
<h3>How many exams attempts do you need?</h3>
</Row>
<Row>
<Col size={6}>
<Form onSubmit={handleSubmit}>
<Input
type="number"
id="keyQuantity"
min="1"
value={quantity}
onChange={handleChange}
/>
</Form>
</Col>
</Row>
<Row>
<p>
Each exam attempt allows you to register for one or more of the
following certifications:
<ul>
<li>CUE.01: Linux</li>
<li>CUE.02: Desktop</li>
<li>CUE.03: Server</li>
</ul>
</p>
</Row>
</Strip>
<section className="p-strip--light is-shallow p-shop-cart">
<Row className="u-sv3">
<Col size={6} className="p-heading--2">
Your Order
</Col>
</Row>
<Row className="u-sv3">
<Col size={6}>{quantity} x Exam attempt key</Col>
<Col size={3}>
{currencyFormatter.format(
((CUEExamKey?.price.value ?? 0) / 100) * (Number(quantity) ?? 0)
)}
</Col>
<Col size={3}>
<Button appearance="positive" type="submit" onClick={handleSubmit}>
Buy Now
</Button>
</Col>
</Row>
</section>
</>
);
};
export default CredKeyShop;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./CredKeyShop";
Loading
Loading