Skip to content

Commit 0566232

Browse files
authored
feat: add plain support integration to the support section (#2597)
1 parent 5c89159 commit 0566232

12 files changed

Lines changed: 338 additions & 57 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ NEXT_PUBLIC_IS_CYPRESS_ENABLED=false
2020
NEXT_PUBLIC_AMPLITUDE_API_KEY=6b28cb736c53d59f0951a50f59597aae
2121
NEXT_PUBLIC_PRIVATE_RPC_ENABLED=false
2222

23+
2324
# Set to 'true' to allow all domains for CORS (use only in development)
2425
CORS_DOMAINS_ALLOWED=false
2526

@@ -40,3 +41,4 @@ SONIC_RPC_API_KEY=
4041
CELO_RPC_API_KEY=
4142
FAMILY_API_KEY=
4243
FAMILY_API_URL=
44+
PLAIN_API_KEY=

pages/api/plain-mutations.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export const UPSERT_CUSTOMER_MUTATION = `
2+
mutation upsertCustomer($input: UpsertCustomerInput!) {
3+
upsertCustomer(input: $input) {
4+
result
5+
customer {
6+
id
7+
externalId
8+
shortName
9+
fullName
10+
email {
11+
email
12+
isVerified
13+
}
14+
status
15+
}
16+
error {
17+
message
18+
type
19+
code
20+
fields {
21+
field
22+
message
23+
type
24+
}
25+
}
26+
}
27+
}
28+
`;
29+
30+
export const CREATE_THREAD_MUTATION = `
31+
mutation createThread($input: CreateThreadInput!) {
32+
createThread(input: $input) {
33+
thread {
34+
id
35+
externalId
36+
customer {
37+
id
38+
}
39+
status
40+
statusChangedAt {
41+
iso8601
42+
unixTimestamp
43+
}
44+
title
45+
previewText
46+
priority
47+
}
48+
error {
49+
message
50+
type
51+
code
52+
fields {
53+
field
54+
message
55+
type
56+
}
57+
}
58+
}
59+
}
60+
`;

pages/api/support-create-ticket.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import type { NextApiRequest, NextApiResponse } from 'next';
2+
3+
import { CREATE_THREAD_MUTATION, UPSERT_CUSTOMER_MUTATION } from './plain-mutations';
4+
5+
const apiKey = process.env.PLAIN_API_KEY;
6+
if (!apiKey) throw new Error('PLAIN_API_KEY env variable is missing');
7+
8+
const isEmail = (v: string) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(v.trim());
9+
10+
const makeGraphQLRequest = async (query: string, variables: Record<string, unknown>) => {
11+
const response = await fetch('https://core-api.uk.plain.com/graphql/v1', {
12+
method: 'POST',
13+
headers: {
14+
'Content-Type': 'application/json',
15+
Authorization: `Bearer ${apiKey}`,
16+
},
17+
body: JSON.stringify({
18+
query,
19+
variables,
20+
}),
21+
});
22+
23+
if (!response.ok) {
24+
throw new Error(`HTTP error! status: ${response.status}`);
25+
}
26+
27+
return response.json();
28+
};
29+
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
30+
const allowedOrigins = ['https://app.aave.com', 'https://aave.com'];
31+
const origin = req.headers.origin;
32+
33+
const isOriginAllowed = (origin: string | undefined): boolean => {
34+
if (!origin) return false;
35+
36+
if (allowedOrigins.includes(origin)) return true;
37+
38+
// Match any subdomain ending with avaraxyz.vercel.app for deployment urls
39+
const allowedPatterns = [/^https:\/\/.*avaraxyz\.vercel\.app$/];
40+
41+
return allowedPatterns.some((pattern) => pattern.test(origin));
42+
};
43+
44+
if (origin && isOriginAllowed(origin)) {
45+
res.setHeader('Access-Control-Allow-Origin', origin);
46+
}
47+
48+
res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
49+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
50+
51+
if (req.method === 'OPTIONS') {
52+
return res.status(200).end();
53+
}
54+
55+
if (req.method !== 'POST') {
56+
return res.status(405).json({ error: 'Method not allowed' });
57+
}
58+
59+
try {
60+
const { email, text } = req.body;
61+
62+
if (!email || !text) {
63+
return res.status(400).json({ message: 'Email and text are required.' });
64+
}
65+
66+
if (!isEmail(email)) {
67+
return res.status(400).json({ message: 'Invalid email format.' });
68+
}
69+
70+
if (!text?.trim()) {
71+
return res.status(400).json({ error: 'Missing inquiry' });
72+
}
73+
74+
const upsertCustomerVariables = {
75+
input: {
76+
identifier: {
77+
emailAddress: email,
78+
},
79+
onCreate: {
80+
fullName: email,
81+
email: {
82+
email: email,
83+
isVerified: true,
84+
},
85+
},
86+
onUpdate: {},
87+
},
88+
};
89+
90+
const customerRes = await makeGraphQLRequest(UPSERT_CUSTOMER_MUTATION, upsertCustomerVariables);
91+
92+
if (customerRes.errors) {
93+
console.error('GraphQL errors:', customerRes.errors);
94+
return res
95+
.status(400)
96+
.json({ message: 'Failed to create support ticket', error: customerRes.errors });
97+
}
98+
const customerResult = customerRes.data?.upsertCustomer;
99+
if (customerResult?.error) {
100+
console.error('Error upserting Customer:', customerResult.error);
101+
return res
102+
.status(400)
103+
.json({ message: 'Failed to create support ticket', error: customerResult.error });
104+
}
105+
106+
const createThreadVariables = {
107+
input: {
108+
title: 'New Support Inquiry',
109+
customerIdentifier: {
110+
emailAddress: email,
111+
},
112+
components: [
113+
{
114+
componentText: {
115+
text: 'Support inquiry from aave.com',
116+
},
117+
},
118+
{
119+
componentDivider: {
120+
dividerSpacingSize: 'M',
121+
},
122+
},
123+
{
124+
componentText: {
125+
textSize: 'S',
126+
textColor: 'MUTED',
127+
text: 'Contact email',
128+
},
129+
},
130+
{
131+
componentText: {
132+
text: email,
133+
},
134+
},
135+
{
136+
componentSpacer: {
137+
spacerSize: 'M',
138+
},
139+
},
140+
{
141+
componentText: {
142+
textSize: 'S',
143+
textColor: 'MUTED',
144+
text: 'Message',
145+
},
146+
},
147+
{
148+
componentPlainText: {
149+
plainText: text,
150+
},
151+
},
152+
{
153+
componentSpacer: {
154+
spacerSize: 'M',
155+
},
156+
},
157+
],
158+
labelTypeIds: ['lt_01K36FQ2J7ZGXQ55RV769TJHYN'],
159+
},
160+
};
161+
const result = await makeGraphQLRequest(CREATE_THREAD_MUTATION, createThreadVariables);
162+
163+
if (result.errors) {
164+
console.error('GraphQL errors in createThread:', result.errors);
165+
return res
166+
.status(400)
167+
.json({ message: 'Failed to create support ticket', error: result.errors });
168+
}
169+
170+
const threadResult = result.data?.createThread;
171+
if (threadResult?.error) {
172+
console.error('Error creating support ticket:', threadResult.error);
173+
return res
174+
.status(400)
175+
.json({ message: 'Failed to create support ticket', error: threadResult.error });
176+
}
177+
178+
return res.status(200).json({
179+
message: 'Support Ticket Created Successfully',
180+
data: threadResult.thread.id,
181+
ok: true,
182+
});
183+
} catch (error) {
184+
console.error('Support ticket backend error:', error);
185+
return res.status(500).json({ message: 'Internal Server Error' });
186+
}
187+
}

src/components/transactions/FlowCommons/BaseSuccess.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export type BaseSuccessTxViewProps = {
1212
hideTx?: boolean;
1313
customExplorerLink?: string;
1414
customExplorerLinkText?: ReactNode;
15+
onClose?: () => void;
1516
};
1617

1718
const ExtLinkIcon = () => (
@@ -26,8 +27,10 @@ export const BaseSuccessView = ({
2627
hideTx,
2728
customExplorerLink,
2829
customExplorerLinkText,
30+
onClose,
2931
}: BaseSuccessTxViewProps) => {
30-
const { close, mainTxState } = useModalContext();
32+
const { close: modalClose, mainTxState } = useModalContext();
33+
const handleClose = onClose || modalClose;
3134
const currentNetworkConfig = useRootStore((store) => store.currentNetworkConfig);
3235

3336
return (
@@ -89,10 +92,10 @@ export const BaseSuccessView = ({
8992
)}
9093

9194
<Button
92-
onClick={close}
95+
onClick={handleClose}
9396
variant="contained"
9497
size="large"
95-
sx={{ minHeight: '50px' }}
98+
sx={{ minHeight: '50px', mb: '30px' }}
9699
data-cy="closeButton"
97100
>
98101
<Trans>Ok, Close</Trans>

src/layouts/AppFooter.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ export function AppFooter() {
7272
},
7373
{
7474
href: 'https://discord.com/invite/aave',
75-
label: <Trans>Send feedback</Trans>,
76-
key: 'Send feedback',
75+
label: <Trans>Get Support</Trans>,
76+
key: 'Get Support',
7777
onClick: (event: React.MouseEvent) => {
7878
event.preventDefault();
7979
setFeedbackOpen(true);

src/layouts/MainLayout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useRouter } from 'next/router';
44
import React, { ReactNode } from 'react';
55
import AnalyticsConsent from 'src/components/Analytics/AnalyticsConsent';
66
// import { useModalContext } from 'src/hooks/useModal';
7-
import { FeedbackModal } from 'src/layouts/FeedbackDialog';
7+
import { SupportModal } from 'src/layouts/SupportModal';
88
import { useRootStore } from 'src/store/root';
99
import { CustomMarket } from 'src/ui-config/marketsConfig';
1010
import { FORK_ENABLED } from 'src/utils/marketsAndNetworksConfig';
@@ -138,7 +138,7 @@ export function MainLayout({ children }: { children: ReactNode }) {
138138
{children}
139139
</Box>
140140
<AppFooter />
141-
<FeedbackModal />
141+
<SupportModal />
142142
{FORK_ENABLED ? null : <AnalyticsConsent />}
143143
</>
144144
);

0 commit comments

Comments
 (0)