suioutkit 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +277 -0
- package/assets/flutterwave.png +0 -0
- package/assets/opay.png +0 -0
- package/assets/stripe.png +0 -0
- package/assets/stripe_c.jpeg +0 -0
- package/assets/sui.png +0 -0
- package/assets/suioutkit.png +0 -0
- package/dist/components/PaymentStatusUI.d.ts +7 -0
- package/dist/components/ProgressStepper.d.ts +10 -0
- package/dist/components/StatusBadge.d.ts +7 -0
- package/dist/components/modal.d.ts +50 -0
- package/dist/config/api.d.ts +5 -0
- package/dist/hooks/usePaymentStatus.d.ts +12 -0
- package/dist/hooks/usePolling.d.ts +13 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +51124 -0
- package/dist/types/index.d.ts +57 -0
- package/dist/utils/format.d.ts +12 -0
- package/dist/utils/http.d.ts +11 -0
- package/package.json +40 -0
- package/src/components/PaymentStatusUI.tsx +58 -0
- package/src/components/ProgressStepper.tsx +23 -0
- package/src/components/StatusBadge.tsx +22 -0
- package/src/components/modal.ts +992 -0
- package/src/components/style.css +751 -0
- package/src/config/api.ts +16 -0
- package/src/declarations.d.ts +1 -0
- package/src/hooks/usePaymentStatus.ts +40 -0
- package/src/hooks/usePolling.ts +46 -0
- package/src/index.ts +139 -0
- package/src/types/index.ts +69 -0
- package/src/utils/format.ts +27 -0
- package/src/utils/http.ts +64 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
// Copyright (c) 2026 The3rdWebLabs (https://github.com/the3rdweblabs)
|
|
3
|
+
// Author: @CYBWithFlourish (https://github.com/CYBWithFlourish)
|
|
4
|
+
|
|
5
|
+
/** Production SuiOutKit API origin (versioned paths under /v1/). */
|
|
6
|
+
//export const DEFAULT_API_ORIGIN = "https://api.suioutkit.xyz";
|
|
7
|
+
export const DEFAULT_API_ORIGIN = "http://localhost:5000";
|
|
8
|
+
|
|
9
|
+
/** API version prefix - all checkout and payment routes live under this path. */
|
|
10
|
+
export const API_V1_PREFIX = "/v1";
|
|
11
|
+
|
|
12
|
+
export function joinApiPath(origin: string, ...segments: string[]): string {
|
|
13
|
+
const base = origin.replace(/\/+$/, "");
|
|
14
|
+
const path = [API_V1_PREFIX, ...segments].join("/").replace(/\/+/g, "/");
|
|
15
|
+
return `${base}${path}`;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module "*.css";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
// Copyright (c) 2026 The3rdWebLabs (https://github.com/the3rdweblabs)
|
|
3
|
+
// Author: @CYBWithFlourish (https://github.com/CYBWithFlourish)
|
|
4
|
+
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { joinApiPath } from "../config/api.js";
|
|
7
|
+
|
|
8
|
+
type PaymentUpdate = {
|
|
9
|
+
status?: "PENDING" | "PROCESSING" | "BANK_CONFIRMED" | "SETTLED" | "ERROR";
|
|
10
|
+
walrusBlobId?: string;
|
|
11
|
+
txDigest?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook to listen to backend payment status via Server‑Sent Events.
|
|
17
|
+
* Usage: const update = usePaymentStatus(session.nonce);
|
|
18
|
+
*/
|
|
19
|
+
export function usePaymentStatus(backendUrl: string, nonce: string): PaymentUpdate {
|
|
20
|
+
const [state, setState] = useState<PaymentUpdate>({ status: "PENDING" });
|
|
21
|
+
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const source = new EventSource(joinApiPath(backendUrl, "payments", "stream", nonce));
|
|
24
|
+
source.onmessage = (e) => {
|
|
25
|
+
try {
|
|
26
|
+
const data = JSON.parse(e.data);
|
|
27
|
+
setState((prev) => ({ ...prev, ...data }));
|
|
28
|
+
} catch (_) { }
|
|
29
|
+
};
|
|
30
|
+
source.onerror = () => {
|
|
31
|
+
if (source.readyState === EventSource.CLOSED) {
|
|
32
|
+
setState((prev) => ({ ...prev, status: "ERROR", error: "Connection lost" }));
|
|
33
|
+
source.close();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
return () => source.close();
|
|
37
|
+
}, [backendUrl, nonce]);
|
|
38
|
+
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
// Copyright (c) 2026 The3rdWebLabs (https://github.com/the3rdweblabs)
|
|
3
|
+
// Author: @CYBWithFlourish (https://github.com/CYBWithFlourish)
|
|
4
|
+
/**
|
|
5
|
+
* Lightweight framework-agnostic polling utility.
|
|
6
|
+
* Usage:
|
|
7
|
+
* const poll = createPolling(async () => { await fetchStatus() }, 5000);
|
|
8
|
+
* poll.start();
|
|
9
|
+
* poll.stop();
|
|
10
|
+
*/
|
|
11
|
+
export function createPolling(fn: () => Promise<void> | void, intervalMs: number) {
|
|
12
|
+
let timer: number | null = null;
|
|
13
|
+
let running = false;
|
|
14
|
+
|
|
15
|
+
async function tick() {
|
|
16
|
+
try {
|
|
17
|
+
await fn();
|
|
18
|
+
} catch (e) {
|
|
19
|
+
// swallow errors — caller should handle inside fn
|
|
20
|
+
// but preserve running state
|
|
21
|
+
}
|
|
22
|
+
if (running) {
|
|
23
|
+
timer = (setTimeout(() => void tick(), intervalMs) as unknown) as number;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
start() {
|
|
29
|
+
if (running) return;
|
|
30
|
+
running = true;
|
|
31
|
+
tick();
|
|
32
|
+
},
|
|
33
|
+
stop() {
|
|
34
|
+
running = false;
|
|
35
|
+
if (timer) {
|
|
36
|
+
clearTimeout(timer as unknown as number);
|
|
37
|
+
timer = null;
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
isRunning() {
|
|
41
|
+
return running;
|
|
42
|
+
}
|
|
43
|
+
} as const;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export default createPolling;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
// Copyright (c) 2026 The3rdWebLabs (https://github.com/the3rdweblabs)
|
|
3
|
+
// Author: @CYBWithFlourish (https://github.com/CYBWithFlourish)
|
|
4
|
+
|
|
5
|
+
import { CheckoutSession, CheckoutSessionOptions, CryptoConfirmResponse } from "./types/index.js";
|
|
6
|
+
import { SuiOutKitModal } from "./components/modal.js";
|
|
7
|
+
import { DEFAULT_API_ORIGIN, joinApiPath } from "./config/api.js";
|
|
8
|
+
|
|
9
|
+
export { DEFAULT_API_ORIGIN, API_V1_PREFIX } from "./config/api.js";
|
|
10
|
+
|
|
11
|
+
export class SuiOutKit {
|
|
12
|
+
private backendUrl: string;
|
|
13
|
+
private merchantAddress: string;
|
|
14
|
+
|
|
15
|
+
constructor(config: { merchantAddress: string; backendUrl?: string }) {
|
|
16
|
+
if (!config.merchantAddress) {
|
|
17
|
+
throw new Error("SuiOutKit Error: merchantAddress is required.");
|
|
18
|
+
}
|
|
19
|
+
// Strip trailing slashes and provide default fallback
|
|
20
|
+
this.backendUrl = (config.backendUrl || DEFAULT_API_ORIGIN).replace(/\/+$/, "");
|
|
21
|
+
this.merchantAddress = config.merchantAddress;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Initializes a brand-new isolated checkout session from the backend.
|
|
26
|
+
*/
|
|
27
|
+
public async initCheckout(options: Omit<CheckoutSessionOptions, "merchantAddress">): Promise<CheckoutSession> {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(joinApiPath(this.backendUrl, "checkout", "session"), {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
amount: options.amount,
|
|
34
|
+
currency: options.currency,
|
|
35
|
+
merchantAddress: this.merchantAddress,
|
|
36
|
+
metadata: options.metadata || {}
|
|
37
|
+
})
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
throw new Error(`Server returned status ${response.status}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return await response.json();
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error("SuiOutKit SDK Init Error:", err);
|
|
47
|
+
throw new Error("SuiOutKit: Failed to initialize checkout session.");
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Spawns the interactive RainbowKit-style checkout modal.
|
|
53
|
+
*/
|
|
54
|
+
public openModal(session: CheckoutSession, onClose?: () => void): SuiOutKitModal {
|
|
55
|
+
return new SuiOutKitModal(session, this.backendUrl, () => {
|
|
56
|
+
if (onClose) onClose();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Confirms a crypto payment after wallet execution by submitting the tx digest.
|
|
62
|
+
*/
|
|
63
|
+
public async confirmCryptoPayment(
|
|
64
|
+
nonce: string,
|
|
65
|
+
txDigest: string,
|
|
66
|
+
method: "sui_wallet" | "outpay" = "sui_wallet"
|
|
67
|
+
): Promise<CryptoConfirmResponse> {
|
|
68
|
+
const response = await fetch(joinApiPath(this.backendUrl, "checkout", "crypto", "confirm"), {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify({ nonce, txDigest, method })
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const result: CryptoConfirmResponse = await response.json();
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
return {
|
|
77
|
+
status: "error",
|
|
78
|
+
error: result.error || "Crypto confirmation failed."
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Integrates dynamically with a targeted button on the merchant's landing page.
|
|
87
|
+
* Brands the button and isolates a unique checkout session per click.
|
|
88
|
+
*/
|
|
89
|
+
public wrapButton(
|
|
90
|
+
selector: string,
|
|
91
|
+
options: { amount: number; currency: "NGN" | "SUI" | string; metadata?: Record<string, any> }
|
|
92
|
+
): void {
|
|
93
|
+
const btn = document.querySelector(selector) as HTMLButtonElement;
|
|
94
|
+
if (!btn) {
|
|
95
|
+
console.warn(`SuiOutKit: Element with selector "${selector}" was not found.`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Format display currency symbol
|
|
100
|
+
const currencySymbol = options.currency === "NGN" ? "₦" : "";
|
|
101
|
+
const formattedAmount = `${currencySymbol}${options.amount.toLocaleString()}`;
|
|
102
|
+
const originalText = btn.textContent || "Pay Now";
|
|
103
|
+
|
|
104
|
+
// Set premium branded text
|
|
105
|
+
btn.textContent = `Pay ${formattedAmount}`;
|
|
106
|
+
|
|
107
|
+
// Add pointer cursor and smooth styling transitions
|
|
108
|
+
btn.style.cursor = "pointer";
|
|
109
|
+
btn.style.transition = "opacity 0.2s ease";
|
|
110
|
+
|
|
111
|
+
btn.addEventListener("click", async () => {
|
|
112
|
+
const tempText = btn.textContent;
|
|
113
|
+
btn.disabled = true;
|
|
114
|
+
btn.textContent = "Loading Checkout...";
|
|
115
|
+
btn.style.opacity = "0.7";
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const session = await this.initCheckout(options);
|
|
119
|
+
this.openModal(session, () => {
|
|
120
|
+
// Restore button when modal is closed
|
|
121
|
+
btn.disabled = false;
|
|
122
|
+
btn.textContent = `Pay ${formattedAmount}`;
|
|
123
|
+
btn.style.opacity = "1";
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
alert("SuiOutKit Error: Unable to open secure payment session.");
|
|
127
|
+
btn.disabled = false;
|
|
128
|
+
btn.textContent = originalText;
|
|
129
|
+
btn.style.opacity = "1";
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Re-export small helpers
|
|
136
|
+
export { default as request } from "./utils/http.js";
|
|
137
|
+
export * from "./utils/format.js";
|
|
138
|
+
export { default as createPolling } from "./hooks/usePolling.js";
|
|
139
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
// Copyright (c) 2026 The3rdWebLabs (https://github.com/the3rdweblabs)
|
|
3
|
+
// Author: @CYBWithFlourish (https://github.com/CYBWithFlourish)
|
|
4
|
+
|
|
5
|
+
export type SomeType = any;
|
|
6
|
+
|
|
7
|
+
export interface CheckoutSessionOptions {
|
|
8
|
+
amount: number;
|
|
9
|
+
currency: "NGN" | "SUI" | string;
|
|
10
|
+
merchantAddress: string;
|
|
11
|
+
metadata?: Record<string, any>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CheckoutSession {
|
|
15
|
+
token: string;
|
|
16
|
+
nonce: string;
|
|
17
|
+
amount: number;
|
|
18
|
+
currency: string;
|
|
19
|
+
merchantAddress: string;
|
|
20
|
+
walrusBlobId?: string;
|
|
21
|
+
packageId?: string;
|
|
22
|
+
cryptoRegistryId?: string;
|
|
23
|
+
cryptoRegistryName?: string;
|
|
24
|
+
coinType?: string;
|
|
25
|
+
estimatedRate?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ChargeMethod = "bank_transfer" | "opay" | "crypto" | "sui_wallet" | "outpay" | "stripe";
|
|
29
|
+
|
|
30
|
+
export interface VirtualAccount {
|
|
31
|
+
accountNumber: string;
|
|
32
|
+
bankName: string;
|
|
33
|
+
amount: number;
|
|
34
|
+
expirySeconds: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ChargeResponse {
|
|
38
|
+
status: "success" | "pending" | "error";
|
|
39
|
+
virtualAccount?: VirtualAccount;
|
|
40
|
+
opayPrompt?: string;
|
|
41
|
+
clientSecret?: string;
|
|
42
|
+
stripePublicKey?: string;
|
|
43
|
+
message?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CryptoIntentResponse {
|
|
47
|
+
nonce: string;
|
|
48
|
+
receiverAddress: string;
|
|
49
|
+
amountBaseUnits: number;
|
|
50
|
+
coinType: string;
|
|
51
|
+
packageId?: string;
|
|
52
|
+
registryName?: string;
|
|
53
|
+
walrusBlobId?: string;
|
|
54
|
+
rate?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CryptoConfirmResponse {
|
|
58
|
+
status: "success" | "error";
|
|
59
|
+
txDigest?: string;
|
|
60
|
+
walrusBlobId?: string;
|
|
61
|
+
error?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CheckoutStatusResponse {
|
|
65
|
+
status: "PENDING" | "PROCESSING" | "SETTLED" | "EXPIRED";
|
|
66
|
+
txDigest?: string;
|
|
67
|
+
walrusBlobId?: string;
|
|
68
|
+
error?: string;
|
|
69
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
// Copyright (c) 2026 The3rdWebLabs (https://github.com/the3rdweblabs)
|
|
3
|
+
// Author: @CYBWithFlourish (https://github.com/CYBWithFlourish)
|
|
4
|
+
|
|
5
|
+
/** Format a Naira amount with currency symbol and grouping. */
|
|
6
|
+
export function formatNgn(amount: number): string {
|
|
7
|
+
try {
|
|
8
|
+
return new Intl.NumberFormat("en-NG", { style: "currency", currency: "NGN", maximumFractionDigits: 0 }).format(amount);
|
|
9
|
+
} catch (_) {
|
|
10
|
+
// Fallback
|
|
11
|
+
return `₦${Math.round(amount).toLocaleString()}`;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Convert base integer units into token float given decimals. */
|
|
16
|
+
export function toTokenUnits(baseUnits: number, decimals = 9): number {
|
|
17
|
+
return baseUnits / Math.pow(10, decimals);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Format token amounts with fixed decimals and trimming. */
|
|
21
|
+
export function formatToken(amount: number, decimals = 9, digits = 6): string {
|
|
22
|
+
const value = Number(amount);
|
|
23
|
+
if (!isFinite(value)) return "0";
|
|
24
|
+
return value.toFixed(digits).replace(/(?:\.0+|(?<=\.[0-9]*?)0+)$/, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default { formatNgn, toTokenUnits, formatToken };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0
|
|
2
|
+
// Copyright (c) 2026 The3rdWebLabs (https://github.com/the3rdweblabs)
|
|
3
|
+
// Author: @CYBWithFlourish (https://github.com/CYBWithFlourish)
|
|
4
|
+
|
|
5
|
+
export type RequestOptions = RequestInit & {
|
|
6
|
+
timeout?: number; // ms
|
|
7
|
+
retries?: number; // simple retry count for idempotent GETs
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class HttpError extends Error {
|
|
11
|
+
status: number | null;
|
|
12
|
+
body: any | null;
|
|
13
|
+
constructor(message: string, status: number | null = null, body: any | null = null) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.body = body;
|
|
17
|
+
this.name = "HttpError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function request<T = any>(input: string, opts: RequestOptions = {}): Promise<T> {
|
|
22
|
+
const { timeout = 10000, retries = 0, ...fetchOpts } = opts;
|
|
23
|
+
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const id = setTimeout(() => controller.abort(), timeout);
|
|
26
|
+
let lastErr: any = null;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(input, { signal: controller.signal, ...fetchOpts } as RequestInit);
|
|
32
|
+
clearTimeout(id);
|
|
33
|
+
|
|
34
|
+
const contentType = res.headers.get("content-type") || "";
|
|
35
|
+
let body: any = null;
|
|
36
|
+
if (contentType.includes("application/json")) {
|
|
37
|
+
body = await res.json();
|
|
38
|
+
} else {
|
|
39
|
+
body = await res.text();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
throw new HttpError(`HTTP ${res.status}: ${res.statusText}`, res.status, body);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return body as T;
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
lastErr = err;
|
|
49
|
+
// If aborted or unrecoverable, break early
|
|
50
|
+
if (err.name === "AbortError") throw new HttpError("Request timed out", null, null);
|
|
51
|
+
// For non-GET methods, don't retry
|
|
52
|
+
const method = (fetchOpts.method || "GET").toUpperCase();
|
|
53
|
+
if (method !== "GET") throw err;
|
|
54
|
+
// Otherwise loop to retry
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
throw lastErr || new Error("Unknown fetch error");
|
|
59
|
+
} finally {
|
|
60
|
+
clearTimeout(id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export default request;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"lib": ["DOM", "ES2020"],
|
|
6
|
+
"rootDir": "./src",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "./dist",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"jsx": "react-jsx",
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"noUncheckedSideEffectImports": true,
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
"include": ["src/**/*"]
|
|
19
|
+
}
|