From 0acfbd61d1f1ccaecf4828f864113dd9bf12f0a0 Mon Sep 17 00:00:00 2001 From: Immanuel Onyeka Date: Sun, 18 Jul 2021 14:21:46 -0400 Subject: [PATCH] Initial commit --- .gitignore | 1 + BillingController.php | 278 ++++++++++++++++ README.md | 6 + main.scss | 745 ++++++++++++++++++++++++++++++++++++++++++ panel.vue | 129 ++++++++ 5 files changed, 1159 insertions(+) create mode 100644 .gitignore create mode 100644 BillingController.php create mode 100644 README.md create mode 100644 main.scss create mode 100644 panel.vue diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45d62d8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.sw? diff --git a/BillingController.php b/BillingController.php new file mode 100644 index 0000000..7042ea2 --- /dev/null +++ b/BillingController.php @@ -0,0 +1,278 @@ +stripe = new \Stripe\StripeClient(config('services.stripe.secret')); + Stripe::setApiKey(config('services.stripe.secret')); + $this->user = Auth::user(); + } + + protected function attempt($packs) { + $user = Auth::user(); + + foreach($packs as $value) { + if ($value < 0) { + abort(422); + } + } + + $amount = $packs[ 'credits10' ]*1099 + + $packs[ 'credits50' ]*5499 + $packs[ 'credits100' ]*10999 + + $packs[ 'credits1000' ]*101000; + + $transaction = new Transaction; + $transaction->credits = $packs['credits10']*1000 + + $packs['credits50']*5000 + + $packs['credits100']*10000 + + $packs['credits1000']*100000; + $transaction->credits_extra = + $packs['credits50']*500 + + $packs['credits100']*1000 + + $packs['credits1000']*15000; + + $transaction->user_id = $user->id; + $transaction->charge = $amount; + $transaction->status = 'processing'; + $transaction->completed = false; + $transaction->save(); + return $transaction; + } + + //Expects an array 'packs' representing the amount of each multiple of credits. + //Should validate that all amounts are positive integers in a reasonable range + public function stripeSecret(Request $request) { + $user = Auth::user(); + $transaction = $this->attempt($request->packs); + + $intent = PaymentIntent::create([ + 'amount' => $amount, + 'currency' => 'usd', + 'customer' => $user->customer_id, + 'description' => "You have received $total_credits credits.", + 'receipt_email' => Auth::user()->email, + 'metadata' => ['transaction_id' => $transaction->id] + ]); + $transaction->intent_id = $intent->id; + + //Save the card as a default if none is set and it was selected + if ($user->payment_method == null && $request->card) { + $this->changeDefaultCard($request->card); + } + + $transaction->save(); + return $intent->client_secret; + } + + public function getCards() { + return PaymentMethod::all([ + 'customer' => Auth::user()->customer_id, + 'type' => 'card' + ]); + } + + //Adds correct credit amount to the charged user, precise to two decimal places + public function chargeEvent(Request $request) { + $event = \Stripe\Event::constructFrom($request->all()); + $charge = $event->data->object; + $transaction = Transaction::where('intent_id', $charge->payment_intent)->first(); + + if ($event->type == 'charge.succeeded') { + $this->creditUser($transaction->id); + } else { + $transaction->status = $charge->status; + $transaction->save(); + } + } + + public function changeDefaultCard(String $card) { + $user = Auth::user(); + $user->payment_method = $card; + $user->save(); + $cards = $this->getCards(); + + return PaymentMethod::all([ + 'customer' => Auth::user()->customer_id, + 'type' => 'card' + ]); + } + + public function deleteCard(Request $request) { + $this->stripe->paymentMethods->detach($request->card); + + $user = Auth::user(); + if ($request->card == $user->payment_method) { + $user->payment_method = null; + $user->save(); + } + return ($this->getCards()); + } + + //Receives a request with a packs. It is an array of each type of credit + //amount to be bought + public function payeer(Request $request) { + $user = Auth::user(); + $transaction = $this->attempt($request->packs); + $shopid = config('services.payeer.id'); + $secret = config('services.payeer.secret'); + $param_key = config('services.payeer.param_key'); + $total = $transaction->credits/100 + $transaction->credits_extra/100; + $description = base64_encode("You will receive $total credits."); + + $arHash = [$shopid, $transaction->id, $transaction->charge/100, 'USD', + $description]; + + $params = ['reference' => ['transaction_id' => $transaction->id]]; + $key = md5($param_key.$transaction->id); + $encodedParams = @urlencode(base64_encode(openssl_encrypt( + json_encode($params), 'AES-256-CBC', $key, OPENSSL_RAW_DATA + ))); + $arHash[] = $encodedParams; + $arHash[] = $secret; + + $signature = strtoupper(hash('sha256', implode(':', $arHash))); + $user->paying = true; $user->save(); + + return [ 'signature' => $signature, 'params' => $encodedParams, 'shop' + => $shopid, 'transaction' => $transaction->id, 'amount' => + $transaction->charge/100, 'description' => $description ]; + } + + //This needs to check the ip of the sender + public function processPayeer(Request $request) { + $allowed = ['185.71.65.92', '185.71.65.189', '149.202.17.210']; + $ipAddress = $request->ip(); + + if (!in_array($ipAddress, $allowed)){ + abort(401); + } + + Log::debug('Processing Payeer payment'); + Log::debug($request); + + $secret = config('services.payeer.secret'); + $arHash = [$request->m_operation_id, + $request->m_operation_ps, + $request->m_operation_day, + $request->m_operation_pay_date, + $request->m_shop, + $request->m_orderid, + $request->m_amount, + $request->m_curr, + $request->m_desc, + $request->m_status + ]; + + if (isset($request->m_params)) { + $arHash[] = $request->m_params; + } + + $arHash[] = $secret; + + $signature = strtoupper(hash('sha256', implode(':', $arHash))); + if ($signature == $request->m_sign && $request->m_status == 'success'){ + $this->creditUser((int) $request->m_orderid); + return $request->m_orderid.'|success'; + } else { + $transaction = Transaction::find($request->orderid); + $transaction->status = 'error'; + $transaction->save(); + return $request->m_orderid.'|error'; + } + } + + //Credits the user of a given transaction id + public function creditUser($transaction_id) { + $transaction = Transaction::find($transaction_id); + + if ($transaction->completed) { + abort(422, 'Bad transaction ID'); + } + + $user = $transaction->user; + $user->credits = $user->credits + $transaction->credits + + $transaction->credits_extra; + $transaction->status = 'completed'; + $transaction->completed = true; + + $user->save(); + $transaction->save(); + } + + public function pm(Request $request) { + $user = Auth::user(); + $account = config('services.pm.account'); + $transaction = $this->attempt($request->packs); + $total = $transaction->credits/100 + + $transaction->credits_extra/100; + $description = "You will receive $total credits."; + + $user->paying = true; $user->save(); + + return ['account' => $account, 'transaction' => $transaction->id, + 'amount' => $transaction->charge/100, 'description' => $description]; + } + + //Handler run after PM payment succeds + public function processPM(Request $request) { + $allowed = ['77.109.141.170', '91.205.41.208', '94.242.216.60', + '78.41.203.75']; + $transaction = Transaction::find($request->PAYMENT_ID); + $secret = config('services.pm.secret'); + + //Check that sender is PM and account the amount was paid to is mine. + if (!in_array($request->ip(), $allowed)){ + abort(401); + } else if ($request->PAYEE_ACCOUNT != config('services.pm.account')) { + abort(422); + } else if (!$transaction->complete) { + abort(422); + } + + Log::debug('Processing PM payment'); + Log::debug($request); + + //Would need to be changed if baggage fields are used + $arHash = [$request->PAYMENT_ID, + $request->PAYEE_ACCOUNT, + $request->PAYMENT_AMOUNT, + $request->PAYMENT_UNITS, + $request->PAYMENT_BATCH_NUMBER, + $request->PAYER_ACCOUNT, + strtoupper(md5($secret)), + $request->TIMESTAMPGMT, + ]; + + $signature = strtoupper(md5(implode(':', $arHash))); + + if ($signature == $request->V2_HASH){ + $this->creditUser((int) $transaction->id); + } else { + abort(422, 'Bad hash'); + } + } + + public function completePM(Request $request) { + return redirect('/panel/#transaction-complete'); + } + + public function failPM(Request $request) { + return redirect('/panel/#transaction-failed'); + } + +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..c01f874 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ + +##Code Examples +Some sample code taken from sites I've completed. They're written in +Javascript, PHP, and SCSS. The examples are incomplete and meant for +demonstrative purposes only as I'm not comfortable share complete code of a +live, potentialy profitable project. diff --git a/main.scss b/main.scss new file mode 100644 index 0000000..c091886 --- /dev/null +++ b/main.scss @@ -0,0 +1,745 @@ +@use "vars"; + +@font-face { + font-family: "PatuaOne"; + src: url("../PatuaOne-Regular.ttf") format("opentype"); + font-style: normal; + font-weight: normal; +} + +html body { + font-family: 'PatuaOne', 'Times New Roman'; + margin: 0; +} + +section { + position: relative; + padding-top: 3em; + padding-bottom: 3em; + overflow: hidden; + padding-right: 15px; + padding-left: 15px; +} + +button { + font-family: 'PatuaOne', 'Times New Roman'; +} + +textarea { + border: 1px solid grey; +} + +h1 { + color: white; +} + +h2 { + color: vars.getColor("brand-orange"); + text-align: center; +} + +h3 { + color: vars.getColor("faded-text"); + // text-transform: uppercase; +} + +ul { + color: vars.getColor("faded-text"); + list-style-type: none; + padding: 0; +} + +input, select { + border: 2px solid black; + border-radius: 20px; + padding: 4px; +} + +.icon { + width: 25px; + margin: 10px; +} + +.note-grey { + color: grey; + font-size: 0.8em; + max-width: 70%; +} + +.services-cards li { + padding-bottom: 8px; + &:before { + width: 30px; + height: 30px; + content: url("../images/green-check.svg"); + display: inline; + margin-right: 5px; + // top: 30px; + } +} + +a { + text-decoration: none; +} + +nav { + position: absolute; + width: 100%; + top: 10px; + display: flex; + flex-flow: row wrap; + align-items: center; + justify-content: space-between; + // background-color: transparent; +} + +nav h1 { + margin: 4px 0 10px 6px; + display: inline-block; +} + +nav.info-page h1 { + margin: 4px 0 10px 6px; + display: inline-block; + color: vars.getColor('brand-orange'); +} + +.formal-font { + font-family: "FreeSans"; +} + +nav #logo { + margin-right: 50px; +} + +nav form { + display: flex; + flex-wrap: wrap; + margin-right: 20px; +} + +nav form div { + margin: 0 10px; +} + +nav form button { + min-width: 5em; +} + +nav form p { + margin-top: 0; +} + +nav form input,label { + opacity: 0.7; +} + +nav .nav-toggle { + position: absolute; + right: 10px; + top: 5px; + display: none; + height: 45px; + width: 45px; + margin-left: auto; + background: url("../images/menu-icon.svg"); + background-size: 100%; + background-size: cover; + color: white; +} + +nav .nav-toggle.toggled { + background: url("../images/cancel-icon2.svg"); + width: 25px; + height: 25px; + background-size: 100%; + background-size: cover; + top: 10px; + right: 15px; + color: white; +} + +div.landing-hero { + top: 0; + height: 50em; + background-color: #ff4e00; + // background-image: linear-gradient(to right bottom, #ff4e00, #fc6200, #f87200, #f58100, #f18e00, #f19507, #f09c0f, #f0a317, #f3a620, #f6a927, #f8ad2e, #fbb034); + background-image: linear-gradient(315deg, #ff4e00 0%, #ec9f05 74%); + z-index: 0; +} + +div.hero-filter { + width: 100%; + height: 100%; +} + +div.hero-filter h2 { + color: black; + opacity: 0.7; + max-width: 10em; + font-size: 2.4em; + position: absolute; + top: 3em; + margin-left: 5%; +} + +div.hero-filter p { + position: absolute; + top: 12em; + margin-left: 5%; + font-size: 20px; + width: 10em; + opacity: 0.7; +} + +div.errors { + background-color: #ececec; + color: vars.getColor("red-alert"); + border: 2px solid vars.getColor("red-alert"); + border-radius: 4px; + min-height: 50px; + position: absolute; + left: 50%; + margin-right: -50%; + top: 100px; + min-width: 30em; + transform: translate(-50%, -50%); +} + +.errors ul { + margin: auto; +} + +form.login { + // font-weight: bold; + color: white; +} + +.cancel-icon { + width: 30px; +} + +form.login input { + margin-left: 2px; + color: white; + background-color: transparent; + border: 2px solid white; + border-radius: 2px; + outline-color: orange +} + +form.login .login-btn { + @include vars.special-button("medium-blue", "dark-blue"); +} + +.hero-filter .register-btn { + @include vars.transparent-button; + display: block; + position: relative; + width: 7em; + top: 60%; + margin-left: auto; + margin-right: auto; +} + +div.blue-background { + background-image: url("../images/black-circle.svg"); + background-size: 10px; + background-color: #0d324d; +} + +section.about-us { + padding: 70px 0; +} + +section.about-us p { + max-width: 600px; + margin-left: auto; + margin-right: auto; + border-top: 2px solid vars.getColor("grey"); + border-bottom: 2px solid vars.getColor("grey"); + padding: 15px; + background-color: #0d324d; + color: white; +} + +section.services-cards { + // margin-bottom: 0; + background-color: lightgrey; + padding-bottom: 4em; + background: linear-gradient(to bottom right, transparent 0%, transparent 50%, #e9e9e9 50%, vars.getColor("light-grey") 100%); +} +section.services-cards .cards{ + display: flex; + flex-flow: wrap; + margin-top: 2em; + gap: 25px; + justify-content: center; + max-width: 1050px; + margin-left: auto; + margin-right: auto; + text-align: center; +} + +section.services-cards h2 { + text-align: center; + padding: 10px; + margin-top: 0; + padding-top: 30px; + color: white; +} + +section.services-cards .card { + width: 280px; + height: 350px; + padding: 18px 9px; + border-radius: 4px; + background: white; + box-shadow: 7px 10px 8px rgb(156 166 175 / 22%); + transition: transform 0.2s; + &:hover { + transform: translateY(-20px); + } + +} + +section.services-cards img { + width: 50px; + height: 50px; +} + +section.services-cards button { + @include vars.brand-button("orange"); +} + +section.features-info { + // height: fit-content; + height: 70em; + background: radial-gradient(ellipse at left, #fff, vars.getColor("light-grey") 80%); +} + +.circle-prop { + height: 70em; + overflow: hidden; + width: 100%; + position: relative; + // transform: translateX(-10em); +} + +.circle-prop svg { + width: 50%; +} + +.features-info img { + width: 600px; + position: absolute; + top: 15%; + right: 4%; +} + +div.info-cards { + position: absolute; + top: 10em; + margin-left: 20%; + width: 70%; +} + +.info-card { + margin-bottom: 6em; + + &:nth-of-type(2) { + margin-left: 13%; + } + + &:nth-of-type(3) { + margin-left: 10%; + } + + &:nth-of-type(4) { + margin-left: 5%; + } + + p { + max-width: 20em; + background: white; + padding: 5px; + border-radius: 4px; + text-align: center; + color: vars.getColor("faded-text2"); + // border: 2px solid vars.getColor("medium-orange"); + @include vars.hovering; + + } +} + +section.panel-infos { + min-height: 40em; + margin-top: 5em; + margin-bottom: 5em; + display: flex; + flex-flow: wrap; + align-items: center; + justify-content: center; + gap: 20px; + + .panel-preview { + // box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px; + @include vars.hovering; + padding: 0; + } +} + +.panel-infos h2 { + width: 100%; +} + + +.panel-info { + max-width: 25em; + margin-top: 4em; +} + +.panel-info img { + width: 50px; + height: 50px; +} + +.panel-info p { + color: vars.getColor("faded-text2"); +} + +section.benefits-info div { + margin: 1em auto; + padding: 1em 1em; + border-top: 2px solid vars.getColor("faded-text2"); + border-bottom: 2px solid vars.getColor("faded-text2"); + max-width: 40em; + color: vars.getColor("faded-text"); +} + +.benefits-info h2 { + margin-bottom: 2em; +} + +section.faq-info { + margin-bottom: 6em; + margin-top: 3em; + min-height: 30em; +} + +.faq-info h2 { + margin-bottom: 2em; +} + +section.faq-info .collapsibles { + display: flex; + flex-flow: wrap; + gap: 2em; + justify-content: center; + max-width: 65em; + width: fit-content; + margin-left: auto; + margin-right: auto; + align-text: center; +} + +.faq-info .collapsible { + text-align: center; + width: 20em; + &:not(.active) .content { + display: none; + } +} + +.collapsible button { + width: 95%; + display: block; + margin-right: auto; + height: 2.5em; + border: 2px solid vars.getColor("brand-orange"); + @include vars.button("light-grey"); + font-size: 17px; + // font-weight: bold; +} + +.collapsible button:after { + content: '\02795'; /* Unicode character for "plus" sign (+) */ + font-size: 13px; + color: white; + float: right; + margin-left: 5px; +} + + +.collapsible .content { + // background-color: vars.getColor("faded-text2"); + border: 2px solid black; + border-radius: 4px; + padding: 4px; + // transition: all 0.2s; + // height: 1px; +} + +footer { + height: 6em; + background-color: vars.getColor("dark-grey"); + padding: 20px; + color: vars.getColor("faded-text2"); +} + +footer .foot-links { + width: 20em; + margin: 2em auto; + text-align: center; +} + +footer a { + margin: 0 8px; + color: vars.getColor("faded-text2"); + &:hover { + color: vars.getColor("faded-text"); + } +} + +.hidden { + display: none; +} + +.loading-icon { + display: block; + width: 50px; + height: 50px; + margin: auto; +} + +.register-area p { + text-align: center; +} + +.register-area .loading-icon { + margin-top: 100px; +} + +div.register-area { + overflow: hidden; + height: 0px; + width: 100%; + z-index: 1; + @include vars.hovering3; + margin-bottom: 2em; + position: fixed; + border-radius: 0; + padding: 0; + opacity: 0; + transition: all 0.2s; + background: white; + h3 { + text-align: center; + } + + label,input { + color: black; + display: block; + border-color: black; + width: 95%; + } + + div { + margin-top: 20px; + margin-bottom: 20px; + width: 100%; + } + + form { + width: 160px; + margin: 20px auto; + } + + .submit-btn { + width: 100%; + background-color: #3bb78f; + @include vars.special-button("light-green", "dark-green"); + } + + .cancel-button { + width: 20px; + height: 20px; + background: url("../images/close-icon-black.svg"); + background-size: cover; + position: absolute; + top: 20px; + right: 20px; + } +} + +section.features-info { + // min-height: 90em; +} + +div.register-area.active { + height: fit-content; + min-height: 20em; + opacity: 1; +} + +.medium-icon { + width: 50px; + height: 50px; + margin-left: auto; + margin-right: auto; + display: block; + top: 50px; +} + +// Make small screens more usable +@media (max-width: 720px) { + nav { display: block; } + + nav form { + // display: none; + width: 190px; + margin-left: auto; + margin-right: auto; + align-items: center; + margin-top: 5em; + opacity: 0; + transition: transform 0.3s; + transform: translateY(-50px); + } + + nav form.active { + display: block; + transform: translateX(0%); + opacity: 1; + // background: white; + // color: black; + } + + nav form div { + margin: 10px auto; + } + + nav div label,input { + display: block; + } + + nav .nav-toggle { + display: block; + } + + div.register-area.active { + display: initial; + } + + div.register-area h1 { + font-size: 1.5rem; + display: inline-block; + color: vars.getColor("brand-orange"); + } + + div.register-area h3 { + text-align: center; + } + + div.register-area .login { + color: vars.getColor("brand-orange"); + display: block; + margin-left: auto; + margin-right: auto; + // margin-top: 1em; + width: 10em; + // top: 40px; + // position: absolute; + } + + div.register-area .login input { + border: 2px solid black; + border-color: black; + color: black; + } + + div.register-area button { + margin-top: 1em; + width: 6em; + } + + div.register-area div { + margin-top: 10px; + margin-bottom: 5px; + margin-left: auto; + margin-right: auto; + } + + .landing-hero div.hero-filter h2 { + top: 4em; + } + + .landing-hero div.hero-filter p { + top: 15em; + } + + .landing-hero div.hero-filter .register-btn { + top: 75%; + } +} + +.info-heading { + text-align: center; + margin-top: 3em; + margin-left: auto; + margin-right: auto; + max-width: 35em; + padding: 3em; + height: 30em; + button { + display: block; + margin: 3em auto; + width: 6em; + @include vars.special-button("light-green", "dark-green"); + } +} + +main.panel { + // min-height: 750px; + height: 100vh; + // max-height: 100vh; + background-image: linear-gradient(315deg, #ff4e00 0%, #ec9f05 74%); +} + +#panel { + top: 50px; + padding: 0 5%; + margin: auto; + position: relative; + max-width: 800px; + height: 90%; + display: flex; + gap: 10px; +} + +#sidebar { + position: relative; + width: 6%; + max-width: 55px; + height: 50%; + color: white; + background: transparent; + display: block; + flex-shrink: 0; + + a { + display: block; + margin: 20px 0; + } + + svg { + width: 100%; + height: 35px; + color: white; + } + + a.selected svg { + color: vars.getColor("brand-orange"); + } +} + diff --git a/panel.vue b/panel.vue new file mode 100644 index 0000000..08e866a --- /dev/null +++ b/panel.vue @@ -0,0 +1,129 @@ + + +