From b076e01cb25cc5854325e8be68506b5d3e1d93e4 Mon Sep 17 00:00:00 2001 From: NevvDevv <61601037+JustAnotherDevv@users.noreply.github.com> Date: Sun, 13 Oct 2024 03:22:11 +0200 Subject: [PATCH 01/34] Update README.md --- README.md | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cd7f526..190cc7c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,25 @@ ## Payment Splitter -## Description +```mermaid +graph TD + A[User] -->|Interacts with| B[React Mobile-first dApp] + B -->|Sends requests to| D[Stellar Network] + D -->|Executes| E[Smart Contract] + E -->|Manages| F[Payment Splitting Logic] + F -->|Sends| G[Stablecoins] + G -->|To| H[Recipients] + + subgraph Frontend + B -->|State Management| I[React Hooks/Context] + B -->|UI Components| J[shadcn/ui] + B -->|Auth| K[Passkeys / Freighter] + end + + subgraph Stellar + D -->|Account Management| N[Stellar Accounts] + D -->|Transaction Handling| O[Stellar SDK] + end +``` ### Links @@ -16,6 +35,13 @@ ## Tech Stack -- +- **Dapp** - React + Vite + shadcn/ui +- **Web3** - Stellar + Soroban smart contracts +- **Auth** - Passkeys ### Getting Started + +- Install dependenciex - `pnpm i` +- Build smart Soroban smart contracts and deploy to chosen network(optional) +- Copy content of `.env.example` to `.env` and fill it out with your variables +- Start dapp with command `pnpm run dev` From d2e6c2592ad41e48a704cf990ad5ab45e70f8316 Mon Sep 17 00:00:00 2001 From: Spoyte Date: Sun, 13 Oct 2024 02:25:53 +0100 Subject: [PATCH 02/34] init smart contract --- contracts/StellarPay.rs | 210 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 contracts/StellarPay.rs diff --git a/contracts/StellarPay.rs b/contracts/StellarPay.rs new file mode 100644 index 0000000..86ab717 --- /dev/null +++ b/contracts/StellarPay.rs @@ -0,0 +1,210 @@ +#![no_std] +use soroban_sdk::{contract, contractimpl, contracttype, Address, Env, Vec, Map, Symbol, log, panic_with_error, contracterror}; + +#[derive(Clone, Debug)] +#[contracttype] +pub struct Group { + group_id: u64, + owner: Address, + total_amount: u64, + members: Vec, +} + +#[derive(Clone)] +#[contracttype] +pub struct Member { + user_id: Symbol, + address: Address, + group_balances: Map, +} + +#[derive(Clone)] +#[contracttype] +pub struct Transaction { + user_id: Symbol, + group_id: u64, + amount: u64, + proof: Symbol, // IPFS link + approvals: Vec, +} + +#[derive(Clone, PartialEq, Eq)] +#[contracttype] +pub enum DataKey { + Group(u64), + Member(Symbol), + Transaction(u64), + LastGroupId, + LastTransactionId, + GroupList, // List of all groups +} + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[repr(u32)] +pub enum Error { + GroupAlreadyExists = 1, + GroupNotFound = 2, + Unauthorized = 3, + MemberAlreadyExists = 4, + MemberNotFound = 5, + UserIdAlreadyExists = 6, + GroupCreationFailed = 7, +} + +#[contract] +pub struct MappingContract; + +#[contractimpl] +impl MappingContract { + pub fn create_group(env: &Env, owner: Address) -> u64 { + log!(env, "Entering create_group function"); + log!(env, "Owner address: {:?}", owner); + + let group_id = Self::get_next_group_id(env); + log!(env, "Generated new group_id: {}", group_id); + + let owner_symbol = Symbol::short("owner"); + log!(env, "Created owner symbol: {:?}", owner_symbol); + + let members = Vec::from_array(env, [owner_symbol]); + log!(env, "Created members vector: {:?}", members); + + let group = Group { + group_id, + owner: owner.clone(), + total_amount: 0, + members: Vec::new(env), + }; + log!(env, "Created new group: {:?}", group); + + // Store the individual group + env.storage().persistent().set(&DataKey::Group(group_id), &group); + log!(env, "Stored individual group in persistent storage"); + + // Update the list of all groups + let mut group_list = env.storage().persistent().get::>(&DataKey::GroupList) + .unwrap_or_else(|| { + log!(env, "Group list not found, creating new list"); + Vec::new(env) + }); + log!(env, "Current group list size: {}", group_list.len()); + + group_list.push_back(group); + log!(env, "Added new group to list. New size: {}", group_list.len()); + + env.storage().persistent().set(&DataKey::GroupList, &group_list); + log!(env, "Stored updated group list in persistent storage"); + + log!(env, "Exiting create_group function. Returning group_id: {}", group_id); + group_id + } + + pub fn create_member(env: &Env, user_id: Symbol, address: Address) { + if env.storage().persistent().has(&DataKey::Member(user_id.clone())) { + panic_with_error!(env, Error::UserIdAlreadyExists); + } + let member = Member { + user_id: user_id.clone(), + address, + group_balances: Map::new(env), + }; + env.storage().persistent().set(&DataKey::Member(user_id), &member); + } + + pub fn add_member_to_group(env: &Env, group_id: u64, user_id: Symbol, caller: Address) { + let mut group = env.storage().persistent().get::(&DataKey::Group(group_id)) + .unwrap_or_else(|| panic_with_error!(env, Error::GroupNotFound)); + + if group.owner != caller { + panic_with_error!(env, Error::Unauthorized); + } + + if group.members.contains(&user_id) { + panic_with_error!(env, Error::MemberAlreadyExists); + } + + group.members.push_back(user_id.clone()); + env.storage().persistent().set(&DataKey::Group(group_id), &group); + + let mut member = env.storage().persistent().get::(&DataKey::Member(user_id.clone())) + .unwrap_or_else(|| panic_with_error!(env, Error::MemberNotFound)); + member.group_balances.set(group_id, 0); + env.storage().persistent().set(&DataKey::Member(user_id), &member); + } + + pub fn add_transaction(env: &Env, user_id: Symbol, group_id: u64, amount: u64, proof: Symbol) -> u64 { + let tx_id = Self::get_next_transaction_id(env); + let transaction = Transaction { + user_id: user_id.clone(), + group_id, + amount, + proof, + approvals: Vec::new(env), + }; + env.storage().persistent().set(&DataKey::Transaction(tx_id), &transaction); + + // Update group total amount + let mut group = env.storage().persistent().get::(&DataKey::Group(group_id)) + .unwrap_or_else(|| panic_with_error!(env, Error::GroupNotFound)); + group.total_amount += amount; + env.storage().persistent().set(&DataKey::Group(group_id), &group); + + // Update member's balance in the group + let mut member = env.storage().persistent().get::(&DataKey::Member(user_id.clone())) + .unwrap_or_else(|| panic_with_error!(env, Error::MemberNotFound)); + let new_balance = member.group_balances.get(group_id).unwrap_or(0) + amount; + member.group_balances.set(group_id, new_balance); + env.storage().persistent().set(&DataKey::Member(user_id.clone()), &member); + + tx_id + } + + pub fn approve_transaction(env: &Env, tx_id: u64, approver_id: Symbol) { + let mut transaction = env.storage().persistent().get::(&DataKey::Transaction(tx_id)) + .unwrap_or_else(|| panic_with_error!(env, Error::GroupNotFound)); + + let group = env.storage().persistent().get::(&DataKey::Group(transaction.group_id)) + .unwrap_or_else(|| panic_with_error!(env, Error::GroupNotFound)); + + if !group.members.contains(&approver_id) { + panic_with_error!(env, Error::Unauthorized); + } + + if !transaction.approvals.contains(&approver_id) { + transaction.approvals.push_back(approver_id); + env.storage().persistent().set(&DataKey::Transaction(tx_id), &transaction); + } + } + + pub fn get_all_groups(env: &Env) -> Vec { + env.storage().persistent().get::>(&DataKey::GroupList) + .unwrap_or_else(|| Vec::new(env)) + } + + fn get_next_group_id(env: &Env) -> u64 { + log!(env, "Entering get_next_group_id function"); + + let last_id = env.storage().persistent().get::(&DataKey::LastGroupId).unwrap_or(0); + log!(env, "Last group ID: {}", last_id); + + let new_id = last_id.checked_add(1).unwrap_or_else(|| { + log!(env, "Group ID overflow occurred"); + panic_with_error!(env, Error::GroupCreationFailed); + }); + log!(env, "New group ID: {}", new_id); + + env.storage().persistent().set(&DataKey::LastGroupId, &new_id); + log!(env, "Stored new last group ID in persistent storage"); + + log!(env, "Exiting get_next_group_id function. Returning new_id: {}", new_id); + new_id + } + + fn get_next_transaction_id(env: &Env) -> u64 { + let last_id = env.storage().persistent().get::(&DataKey::LastTransactionId).unwrap_or(0); + let new_id = last_id.checked_add(1).unwrap_or_else(|| panic_with_error!(env, Error::GroupCreationFailed)); + env.storage().persistent().set(&DataKey::LastTransactionId, &new_id); + new_id + } +} \ No newline at end of file From 0a52dd5ead2662b0fdd20fbe86c3a9da2a9636c3 Mon Sep 17 00:00:00 2001 From: JustAnotherDevv Date: Sun, 13 Oct 2024 03:11:52 +0100 Subject: [PATCH 03/34] Improved animations --- dapp/src/components/Layout.tsx | 44 +++++----- dapp/src/pages/Create.tsx | 147 +++++++++++++++++++++++---------- dapp/src/pages/Profile.tsx | 120 +++++++++++++++++---------- 3 files changed, 207 insertions(+), 104 deletions(-) diff --git a/dapp/src/components/Layout.tsx b/dapp/src/components/Layout.tsx index fee70bc..f6a4cb5 100644 --- a/dapp/src/components/Layout.tsx +++ b/dapp/src/components/Layout.tsx @@ -6,7 +6,7 @@ import { motion } from "framer-motion"; import { useStellarWallets } from "@/context/StellarWalletsContext"; import { ISupportedWallet } from "@creit.tech/stellar-wallets-kit"; import { truncateStr } from "@/utils"; -import { Link } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; const Logo = () => (
@@ -20,6 +20,7 @@ const Logo = () => ( ); const NavItem = ({ icon: Icon, label, path, isActive, isMobile, onClick }) => { + const location = useLocation(); return ( { transition={{ type: "spring", stiffness: 400, damping: 17 }} >
- +
+ +
+ Powered By Stellar +
+
); }; diff --git a/dapp/src/pages/Create.tsx b/dapp/src/pages/Create.tsx index 1d4e574..fcca05f 100644 --- a/dapp/src/pages/Create.tsx +++ b/dapp/src/pages/Create.tsx @@ -19,6 +19,7 @@ import { } from "@/components/ui/select"; import { useToast } from "@/hooks/use-toast"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { motion, AnimatePresence } from "framer-motion"; type Expense = { id: number; @@ -32,6 +33,9 @@ type Participant = { name: string; }; +const MotionCard = motion(Card); +const MotionButton = motion(Button); + export default function Create() { const [expenses, setExpenses] = useState([]); const [participants, setParticipants] = useState([]); @@ -162,8 +166,18 @@ export default function Create() { }; return ( -
- + + Add Participants @@ -174,28 +188,48 @@ export default function Create() { value={newParticipant} onChange={(e) => setNewParticipant(e.target.value)} /> - -
-
- {participants.map((participant) => ( -
- {participant.name} -
- ))} +
+ + + {participants.map((participant) => ( + + {participant.name} + + ))} + + - + - + Expense Details @@ -332,9 +366,14 @@ export default function Create() { )} {splitType === "manual" && ( - + )}
@@ -348,13 +387,15 @@ export default function Create() { className="hidden" ref={fileInputRef} /> - + {receiptImage && ( Image uploaded @@ -375,36 +416,56 @@ export default function Create() { {splitType === "manual" && ( -
- {expenses.map((expense) => ( -
- - {expense.description} - ${expense.amount.toFixed(2)} (Paid - by {expense.paidBy}) - - -
- ))} -
+ + {expense.description} - ${expense.amount.toFixed(2)} (Paid + by {expense.paidBy}) + + removeExpense(expense.id)} + whileHover={{ scale: 1.1 }} + whileTap={{ scale: 0.9 }} + > + + + + ))} + +
)} - + - -
+ + ); } diff --git a/dapp/src/pages/Profile.tsx b/dapp/src/pages/Profile.tsx index 5a7573e..ca3d583 100644 --- a/dapp/src/pages/Profile.tsx +++ b/dapp/src/pages/Profile.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; +import { motion, AnimatePresence } from "framer-motion"; type Group = { id: number; @@ -11,6 +12,9 @@ type Group = { isCreator: boolean; }; +const MotionCard = motion(Card); +const MotionButton = motion(Button); + export default function Profile() { const [groups, setGroups] = useState([ { @@ -53,48 +57,80 @@ export default function Profile() { }; return ( -
-

Your Expense Groups

- {groups.map((group) => ( - - - {group.name} - - -
- Total Amount: ${group.totalAmount} - Paid Amount: ${group.paidAmount} -
-
-
-
- {group.isCreator ? ( - - ) : ( - - )} -
-
- ))} -
+ Total Amount: ${group.totalAmount} + Paid Amount: ${group.paidAmount} + +
+ +
+ {group.isCreator ? ( + handleWithdraw(group.id)} + className="mt-4 w-full text-gray-700" + disabled={group.paidAmount < group.totalAmount} + variant="outline" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Withdraw Funds + + ) : ( + handlePayment(group.id)} + className="mt-4 w-full text-gray-700" + disabled={group.paidAmount === group.totalAmount} + variant="outline" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Pay Your Share + + )} + +
+ ))} + + ); } From 4c98c71b82be0a8da2be0a771a5d76852bb5c6aa Mon Sep 17 00:00:00 2001 From: Spoyte Date: Sun, 13 Oct 2024 03:24:57 +0100 Subject: [PATCH 04/34] new test, set_member is the only one working --- contracts/StellarPay2.rs | 98 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 contracts/StellarPay2.rs diff --git a/contracts/StellarPay2.rs b/contracts/StellarPay2.rs new file mode 100644 index 0000000..ec2c8ad --- /dev/null +++ b/contracts/StellarPay2.rs @@ -0,0 +1,98 @@ +#![no_std] +use soroban_sdk::{contract, contracttype, contractimpl, Address, Env, Map, String, Symbol, Vec, Error}; + +#[contracttype] +pub enum DataKey { + Member(Address), + Group(Symbol), + Transaction(Address, Symbol, u32), +} + +#[contracttype] +pub struct Member { + nickname: String, + address: Address, +} + +#[contracttype] +pub struct Group { + group_id: Symbol, + owner: Address, + total_amount: i128, + members: Map, +} + +#[contracttype] +pub struct Transaction { + user_id: Address, + group_id: Symbol, + amount: i128, + proof: String, + approvals: Vec
, +} + +#[contract] +pub struct Contract; + +#[contractimpl] +impl Contract { + // Member functions + pub fn set_member(env: Env, address: Address, nickname: String) { + let member = Member { nickname, address: address.clone() }; + env.storage().instance().set(&DataKey::Member(address), &member); + } + + pub fn get_member(env: Env, address: Address) -> Result { + env.storage().instance().get(&DataKey::Member(address)).ok_or_else(|| Error::from_contract_error(1)) + } + + // Group functions + pub fn set_group(env: Env, group_id: Symbol, owner: Address, total_amount: i128, members: Map) { + let group = Group { group_id: group_id.clone(), owner, total_amount, members }; + env.storage().instance().set(&DataKey::Group(group_id), &group); + } + + pub fn get_group(env: Env, group_id: Symbol) -> Result { + env.storage().instance().get(&DataKey::Group(group_id)).ok_or_else(|| Error::from_contract_error(2)) + } + + pub fn add_group_member(env: Env, group_id: Symbol, member_address: Address, balance: i128) -> Result<(), Error> { + let mut group: Group = env.storage().instance().get(&DataKey::Group(group_id.clone())).ok_or_else(|| Error::from_contract_error(2))?; + group.members.set(member_address, balance); + env.storage().instance().set(&DataKey::Group(group_id), &group); + Ok(()) + } + + pub fn update_group_member_balance(env: Env, group_id: Symbol, member_address: Address, new_balance: i128) -> Result<(), Error> { + let mut group: Group = env.storage().instance().get(&DataKey::Group(group_id.clone())).ok_or_else(|| Error::from_contract_error(2))?; + group.members.set(member_address, new_balance); + env.storage().instance().set(&DataKey::Group(group_id), &group); + Ok(()) + } + + // Transaction functions + pub fn set_transaction(env: Env, user_id: Address, group_id: Symbol, amount: i128, proof: String) { + let transaction = Transaction { + user_id: user_id.clone(), + group_id: group_id.clone(), + amount, + proof, + approvals: Vec::new(&env), + }; + let key = DataKey::Transaction(user_id, group_id, env.ledger().sequence()); + env.storage().instance().set(&key, &transaction); + } + + pub fn get_transaction(env: Env, user_id: Address, group_id: Symbol, sequence: u32) -> Result { + let key = DataKey::Transaction(user_id, group_id, sequence); + env.storage().instance().get(&key).ok_or_else(|| Error::from_contract_error(3)) + } + + pub fn add_approval(env: Env, user_id: Address, group_id: Symbol, sequence: u32, approver: Address) -> Result<(), Error> { + let key = DataKey::Transaction(user_id, group_id, sequence); + let mut transaction: Transaction = env.storage().instance().get(&key).ok_or_else(|| Error::from_contract_error(3))?; + transaction.approvals.push_back(approver); + env.storage().instance().set(&key, &transaction); + Ok(()) + } +} \ No newline at end of file From 205173621f7e6dfb831fccb4ed93ba65f47f9213 Mon Sep 17 00:00:00 2001 From: JustAnotherDevv Date: Sun, 13 Oct 2024 03:32:58 +0100 Subject: [PATCH 05/34] user profile --- dapp/package.json | 1 + dapp/pnpm-lock.yaml | 28 +++++++++++ dapp/src/components/Layout.tsx | 7 ++- dapp/src/components/ui/avatar.tsx | 48 ++++++++++++++++++ dapp/src/pages/Home.tsx | 82 +++++++++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 dapp/src/components/ui/avatar.tsx diff --git a/dapp/package.json b/dapp/package.json index 4db266c..a2b8174 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@creit.tech/stellar-wallets-kit": "^1.2.3", + "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", diff --git a/dapp/pnpm-lock.yaml b/dapp/pnpm-lock.yaml index 5bd18d8..909e06e 100644 --- a/dapp/pnpm-lock.yaml +++ b/dapp/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@creit.tech/stellar-wallets-kit': specifier: ^1.2.3 version: 1.2.3(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-avatar': + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-checkbox': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -609,6 +612,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-avatar@1.1.1': + resolution: {integrity: sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-checkbox@1.1.2': resolution: {integrity: sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==} peerDependencies: @@ -2906,6 +2922,18 @@ snapshots: '@types/react': 18.3.11 '@types/react-dom': 18.3.0 + '@radix-ui/react-avatar@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.11)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.0 + '@radix-ui/react-checkbox@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 diff --git a/dapp/src/components/Layout.tsx b/dapp/src/components/Layout.tsx index f6a4cb5..c945149 100644 --- a/dapp/src/components/Layout.tsx +++ b/dapp/src/components/Layout.tsx @@ -158,21 +158,20 @@ const Navigation = ({ isMobile, activePage, setActivePage }) => {
- + */}
Powered By Stellar
diff --git a/dapp/src/components/ui/avatar.tsx b/dapp/src/components/ui/avatar.tsx new file mode 100644 index 0000000..991f56e --- /dev/null +++ b/dapp/src/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/dapp/src/pages/Home.tsx b/dapp/src/pages/Home.tsx index ef57929..7ccdf20 100644 --- a/dapp/src/pages/Home.tsx +++ b/dapp/src/pages/Home.tsx @@ -10,6 +10,7 @@ import { CreditCard, Activity, Plus, + Wallet, ArrowRightLeft, } from "lucide-react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -17,8 +18,13 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { useStellarWallets } from "@/context/StellarWalletsContext"; import { truncateStr } from "@/utils"; +import { useMediaQuery } from "react-responsive"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +const MotionCard = motion(Card); +const MotionButton = motion(Button); const PaymentSplitterBanner = () => { + const isMobile = useMediaQuery({ maxWidth: 768 }); const [userName, setUserName] = useState(""); const [stellarAddress, setStellarAddress] = useState(""); const stellarWalletsKit = useStellarWallets(); @@ -72,6 +78,27 @@ const PaymentSplitterBanner = () => { setTimeout(() => setUserName("Alex"), 1000); }, []); + const handleConnect = async () => { + try { + if (stellarAddress != "") { + stellarWalletsKit.disconnect(); + setStellarAddress(""); + return; + } + console.log(stellarWalletsKit); + await stellarWalletsKit.openModal({ + onWalletSelected: async (option: ISupportedWallet) => { + stellarWalletsKit.setWallet(option.id); + const { address } = await stellarWalletsKit.getAddress(); + console.log(address); + setStellarAddress(address); + }, + }); + } catch (error) { + console.error("Failed to connect wallet:", error); + } + }; + return (
@@ -121,6 +148,61 @@ const PaymentSplitterBanner = () => {
+
+ + + + + User Profile + + + +
+
+ + + JD + +
+

Jack Doe

+

@jack_doe

+
+
+
+
+ + 5 Groups +
+ +
+
+
+
+
+
Date: Sun, 13 Oct 2024 03:37:27 +0100 Subject: [PATCH 06/34] fix --- dapp/src/components/Layout.tsx | 2 +- dapp/src/pages/Home.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dapp/src/components/Layout.tsx b/dapp/src/components/Layout.tsx index c945149..f154e12 100644 --- a/dapp/src/components/Layout.tsx +++ b/dapp/src/components/Layout.tsx @@ -108,7 +108,7 @@ const Navigation = ({ isMobile, activePage, setActivePage }) => { const navItems = [ { icon: Home, label: "Home", path: "/" }, { icon: Plus, label: "Create", path: "/create" }, - { icon: User, label: "Profile", path: "/profile" }, + { icon: User, label: "Groups", path: "/profile" }, ]; const navClass = isMobile diff --git a/dapp/src/pages/Home.tsx b/dapp/src/pages/Home.tsx index 7ccdf20..6f7d8a9 100644 --- a/dapp/src/pages/Home.tsx +++ b/dapp/src/pages/Home.tsx @@ -293,8 +293,8 @@ const PaymentSplitterBanner = () => {
-
-
+
+

All the features in one app

From 91a9ff6235721f91ec81294285759a43b7953f8e Mon Sep 17 00:00:00 2001 From: JustAnotherDevv Date: Sun, 13 Oct 2024 04:09:29 +0100 Subject: [PATCH 07/34] small changes --- dapp/.gitignore | 3 +- dapp/env.example | 7 +++ dapp/src/components/Layout.tsx | 25 ++++++----- dapp/src/context/StellarWalletsContext.tsx | 4 +- dapp/src/pages/Home.tsx | 50 +++++++++++++++++----- dapp/src/pages/Profile.tsx | 19 +++++--- 6 files changed, 75 insertions(+), 33 deletions(-) create mode 100644 dapp/env.example diff --git a/dapp/.gitignore b/dapp/.gitignore index 3b0b403..f8dd907 100644 --- a/dapp/.gitignore +++ b/dapp/.gitignore @@ -23,4 +23,5 @@ dist-ssr *.sln *.sw? -.env \ No newline at end of file +.env +.vercel diff --git a/dapp/env.example b/dapp/env.example new file mode 100644 index 0000000..f838dca --- /dev/null +++ b/dapp/env.example @@ -0,0 +1,7 @@ +VITE_PUBLIC_networkPassphrase="Test SDF Future Network ; October 2022" +VITE_PUBLIC_chickenVsEggContractId="CADVRBYFMG6RIRFP6VOAATDCZERWPCNGFKQ7KYPD3D4FAI7VZ7WFNDSB" +VITE_PUBLIC_factoryContractId="CCZWIOWKT4WGJQHWZFF7ARCQJFVWRXPOKG4WGY6DOZ72OHZEMKXAEGRO" +VITE_PUBLIC_accountSecp256r1ContractWasm="23d8e1fbdb0bb903815feb7d07b675db98b5376feedab056aab61910d41e80c1" +VITE_PUBLIC_rpcUrl="https://rpc-futurenet.stellar.org/" +VITE_PUBLIC_horizonUrl="https://horizon-futurenet.stellar.org/" +VITE_PUBLIC_INFURA="" \ No newline at end of file diff --git a/dapp/src/components/Layout.tsx b/dapp/src/components/Layout.tsx index f154e12..8924201 100644 --- a/dapp/src/components/Layout.tsx +++ b/dapp/src/components/Layout.tsx @@ -1,11 +1,12 @@ -import React, { useEffect, useState } from "react"; +// @ts-nocheck +import { useEffect, useState } from "react"; import { useMediaQuery } from "react-responsive"; -import { Home, Plus, User, Wallet } from "lucide-react"; +import { Home, Plus, User } from "lucide-react"; import { Button } from "@/components/ui/button"; import { motion } from "framer-motion"; import { useStellarWallets } from "@/context/StellarWalletsContext"; import { ISupportedWallet } from "@creit.tech/stellar-wallets-kit"; -import { truncateStr } from "@/utils"; +// import { truncateStr } from "@/utils"; import { Link, useLocation } from "react-router-dom"; const Logo = () => ( @@ -22,23 +23,21 @@ const Logo = () => ( const NavItem = ({ icon: Icon, label, path, isActive, isMobile, onClick }) => { const location = useLocation(); return ( - + - - )} - {step === 5 && ( { className="text-center" >

Register or Sign In

-

Use your device's biometrics to create or access your account.

+

+ Use your device's biometrics to create or access your account. +

)} - - - + -
- +
); }; -export default SoroPass; \ No newline at end of file +export default SoroPass; diff --git a/dapp/src/pages/Create.tsx b/dapp/src/pages/Create.tsx index b109d0c..7fb16cb 100644 --- a/dapp/src/pages/Create.tsx +++ b/dapp/src/pages/Create.tsx @@ -159,6 +159,7 @@ export default function Create() { const handleImageUpload = async ( event: React.ChangeEvent ) => { + if (!event?.target?.files) return null; const file = event?.target?.files[0]; if (file) { try { @@ -171,19 +172,11 @@ export default function Create() { const ipfsHash = await uploadImageToInfura(file); console.log("IPFS Hash:", ipfsHash); setIpfsCid(ipfsHash); + console.log(ipfsCid); } catch (error) { console.error("Upload failed:", error); } } - - // const file = event.target.files?.[0]; - // if (file) { - // const reader = new FileReader(); - // reader.onloadend = () => { - // setReceiptImage(reader.result as string); - // }; - // reader.readAsDataURL(file); - // } }; return ( @@ -193,11 +186,57 @@ export default function Create() { animate={{ opacity: 1 }} transition={{ duration: 0.5 }} > +
+
+ +
+ + New Group + + + Split expenses, not friendships! 💸🤝 + +
+
Add Participants @@ -243,6 +282,36 @@ export default function Create() { + {
{
-
+
{ whileHover={{ scale: 1.02 }} > - + User Profile -
-
+
+
{ /> JD -
+

Jack Doe

@jack_doe

-
+
5 Groups
@@ -187,7 +234,7 @@ const PaymentSplitterBanner = () => { 5 Groups
- */} + + {isAuthenticated.toString()}
diff --git a/dapp/src/pages/Landing.tsx b/dapp/src/pages/Landing.tsx new file mode 100644 index 0000000..2b2ad6f --- /dev/null +++ b/dapp/src/pages/Landing.tsx @@ -0,0 +1,236 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; +import { ChevronRight, Coins, Users, Zap } from "lucide-react"; +import { useAuth } from "@/hooks/use-auth"; +import { Input } from "@/components/ui/input"; +const Landing = () => { + const { + isAuthenticated, + connection, + userAddress, + bundlerKey, + register, + signIn, + signOut, + initializeBundler, + } = useAuth(); + const [isRegistering, setIsRegistering] = useState(false); + const [username, setUsername] = useState(""); + + const handleRegister = (e: React.FormEvent) => { + e.preventDefault(); + console.log("Registering with username:", username); + register(); + }; + + const handleWalletLogin = () => { + console.log("Logging in with wallet"); + signIn(); + }; + + const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { staggerChildren: 0.1 }, + }, + }; + + const itemVariants = { + hidden: { y: 20, opacity: 0 }, + visible: { + y: 0, + opacity: 1, + transition: { type: "spring", stiffness: 100 }, + }, + }; + + return ( +
+ {/*
*/} + + {/* Background Image Overlay */} +
+ + + +
+
+

+ CryptoSplit +

+

+ Split expenses with crypto, revolutionize your finances +

+ {/*
+ + +
*/} + + + + {isRegistering ? "Register" : "Login"} + + + Choose your authentication method + + + + + {isRegistering ? ( +
+ setUsername(e.target.value)} + className="bg-gray-800 border-gray-700 text-white placeholder-gray-500" + /> + +
+ ) : ( + + )} +
+ + + +
+
+
+
+
+ + + + + + About CryptoSplit + + + +

+ CryptoSplit is the ultimate solution for splitting expenses + using cryptocurrencies. Whether you're traveling with friends, + sharing a house, or managing group purchases, CryptoSplit makes + it easy to keep track of expenses and settle debts using your + favorite cryptocurrencies. +

+
+
+
+ + + + Features + + {[ + { + icon: Coins, + title: "Multi-Crypto Support", + description: "Split expenses using various cryptocurrencies", + }, + { + icon: Users, + title: "Group Management", + description: "Create and manage expense groups easily", + }, + { + icon: Zap, + title: "Passkey Support", + description: "Settle debts quickly with Passkey-powered wallets", + }, + ].map((feature, index) => ( + + + + +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ {/* */} +
+
+
+ ))} +
+ + + Built for EasyA Stellar London hackathon + +
+
+ ); +}; + +export default Landing; diff --git a/dapp/src/pages/Profile.tsx b/dapp/src/pages/Profile.tsx index be6ff11..cf05f2e 100644 --- a/dapp/src/pages/Profile.tsx +++ b/dapp/src/pages/Profile.tsx @@ -65,14 +65,59 @@ export default function Profile() { animate={{ opacity: 1 }} transition={{ duration: 0.5 }} > - +
+ +
+ + Your Expense Groups + + + Where friends split bills and HODL together! 🚀💰 + +
+
+ {/* Your Expense Groups - +
*/} {groups.map((group) => ( {group.isCreator ? ( - handleWithdraw() + onClick={ + () => handleWithdraw() // group.id } className="mt-4 w-full text-gray-700" diff --git a/dapp/src/pages/Settings.tsx b/dapp/src/pages/Settings.tsx deleted file mode 100644 index e69de29..0000000 diff --git a/dapp/src/utils.ts b/dapp/src/utils.ts index a7eb113..2f3c61c 100644 --- a/dapp/src/utils.ts +++ b/dapp/src/utils.ts @@ -1,4 +1,56 @@ import axios from "axios"; +import { SorobanRpc } from "@stellar/stellar-sdk"; +import { + Keypair, + xdr, + Address, + Operation, + TransactionBuilder, + Account, + scValToNative, +} from "@stellar/stellar-sdk"; +import { splitterContract } from "./constants"; + +export const contractRead = async ( + bundlerKey: Keypair, + // accountContractId: string, + functionName: string, + functionArgs: any[] +) => { + const key = bundlerKey; + + const op = Operation.invokeContractFunction({ + contract: splitterContract, + function: functionName, //"votes", + args: functionArgs, + // [ + // xdr.ScVal.scvAddress(Address.fromString(accountContractId).toScAddress()), + // ], + }); + + const transaction = new TransactionBuilder( + new Account(key.publicKey(), "0"), + { + fee: "0", + networkPassphrase: import.meta.env.VITE_PUBLIC_networkPassphrase, + } + ) + .addOperation(op) + .setTimeout(0) + .build(); + + const rpc = new SorobanRpc.Server(import.meta.env.VITE_PUBLIC_rpcUrl); + + const simResp = await rpc.simulateTransaction(transaction); + + if (!SorobanRpc.Api.isSimulationSuccess(simResp)) { + throw simResp; + } else { + return simResp; + } +}; + +export const fetchGroups = async () => {}; export const uploadImageToInfura = async (imageFile: any) => { const formData = new FormData(); From 7cb2659d0049bfd74ba00d31a0621057340ad95d Mon Sep 17 00:00:00 2001 From: cindytrang Date: Sun, 13 Oct 2024 10:46:45 +0100 Subject: [PATCH 19/34] Added the api post to the ocr recepit system --- dapp/src/pages/Create.tsx | 102 ++++++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 11 deletions(-) diff --git a/dapp/src/pages/Create.tsx b/dapp/src/pages/Create.tsx index 7fb16cb..964bd07 100644 --- a/dapp/src/pages/Create.tsx +++ b/dapp/src/pages/Create.tsx @@ -20,7 +20,8 @@ import { import { useToast } from "@/hooks/use-toast"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { motion, AnimatePresence } from "framer-motion"; -import { uploadImageToInfura } from "@/utils"; +// import { uploadImageToInfura } from "@/utils"; +import axios from 'axios'; type Expense = { id: number; @@ -52,6 +53,10 @@ export default function Create() { const [receiptImage, setReceiptImage] = useState(null); const fileInputRef = useRef(null); const { toast } = useToast(); + const [ocrItems, setOcrItems] = useState>([]); + const [ocrTotalAmount, setOcrTotalAmount] = useState(0); + const [ocrMerchantName, setOcrMerchantName] = useState(''); + const [ocrDate, setOcrDate] = useState(''); const addExpense = () => { if (newExpense.description && newExpense.amount > 0 && newExpense.paidBy) { @@ -137,9 +142,9 @@ export default function Create() { Object.entries(balances).forEach(([person, balance]) => { if (balance > 0) { - messages.push(`${person} is owed ${balance.toFixed(2)}`); + messages.push(`${person} is owed £${balance.toFixed(2)}`); } else if (balance < 0) { - messages.push(`${person} owes ${Math.abs(balance).toFixed(2)}`); + messages.push(`${person} owes £${Math.abs(balance).toFixed(2)}`); } }); @@ -156,9 +161,7 @@ export default function Create() { }); }; - const handleImageUpload = async ( - event: React.ChangeEvent - ) => { + const handleImageUpload = async (event: React.ChangeEvent) => { if (!event?.target?.files) return null; const file = event?.target?.files[0]; if (file) { @@ -168,17 +171,94 @@ export default function Create() { setReceiptImage(reader.result as string); }; reader.readAsDataURL(file); - - const ipfsHash = await uploadImageToInfura(file); - console.log("IPFS Hash:", ipfsHash); - setIpfsCid(ipfsHash); - console.log(ipfsCid); + + const formData = new FormData(); + formData.append('api_key', 'TEST'); // Replace 'TEST' with your actual API key + formData.append('recognizer', 'auto'); + formData.append('ref_no', `ocr_react_${Date.now()}`); + formData.append('file', file); + + const response = await axios.post('https://ocr.asprise.com/api/v1/receipt', formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + + const ocrResult = response.data; + console.log("OCR Result:", ocrResult); + + processOcrResult(ocrResult); + } catch (error) { console.error("Upload failed:", error); } } }; + const processOcrResult = (ocrResult: any) => { + if (ocrResult && ocrResult.receipts && ocrResult.receipts.length > 0) { + const receipt = ocrResult.receipts[0]; + + const items = receipt.items?.map((item: any) => ({ + description: item.description || 'Unknown Item', + amount: parseFloat(item.amount) || 0 + })) || []; + setOcrItems(items); + + const totalAmount = parseFloat(receipt.total) || 0; + setOcrTotalAmount(totalAmount); + setTotalAmount(totalAmount); + setOcrMerchantName(receipt.merchant_name || 'Unknown Merchant'); + + setOcrDate(receipt.date || 'Unknown Date'); + + const itemsTotal = items.reduce((sum, item) => sum + item.amount, 0); + const difference = Math.abs(totalAmount - itemsTotal); + + if (difference > 0.01) { // Allow for small rounding differences + toast({ + title: "Receipt Discrepancy", + description: `There's a difference of ${difference.toFixed(2)} between items total and receipt total.`, + variant: "warning", + }); + } + + displayOcrResults(items, totalAmount, itemsTotal); + } + }; + + const displayOcrResults = (items: Array<{ description: string; amount: number }>, totalAmount: number, itemsTotal: number) => { + toast({ + title: "Receipt Processed", + description: ( +
+

Merchant: {ocrMerchantName.toString()}

+

Items: {items.length}

+

Items Total: £{itemsTotal.toFixed(2)}

+

Receipt Total: £{totalAmount.toFixed(2)}

+ +
+ ), + duration: 10000, + }); + }; + + const showDetailedResults = (items: Array<{ description: string; amount: number }>) => { + toast({ + title: "Detailed Receipt Items", + description: ( +
    + {items.map((item, index) => ( +
  • {item.description}: ${item.amount.toFixed(2)}
  • + ))} +
+ ), + duration: 15000, + }); + }; + return ( Date: Sun, 13 Oct 2024 10:50:35 +0100 Subject: [PATCH 20/34] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6691c50..4ced0d4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@


- + logo

Group Splitting Payment App.

@@ -47,10 +47,9 @@ graph TD ### Links -- [Live Demo](https://www.youtube.com/watch?v=EyXrEwHLkoo) +- [Demo](https://www.youtube.com/watch?v=EyXrEwHLkoo) - [Pitch Deck](https://www.canva.com/design/DAGTabHuIc4/BDLopYPbazWElCHx-Q-Z7A/edit?utm_content=DAGTabHuIc4&utm_campaign=designshare&utm_medium=link2&utm_source=sharebutton ) -- Video ### Problem From 8b5fa88e4f27e38c171497d06abfccdca14dfac9 Mon Sep 17 00:00:00 2001 From: Cindy Cieniek <122636657+cindytrang@users.noreply.github.com> Date: Sun, 13 Oct 2024 10:51:44 +0100 Subject: [PATCH 21/34] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ced0d4..78e69a7 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ One key challenge faced by users in big groups or those in a rush is the time an The app’s streamlined process allows users to split expenses and execute payments on the go, making it perfect for situations where time is of the essence, like dining with friends, travelling, or group activities. By leveraging blockchain technology and automation, the app ensures smooth, secure transactions while minimizing the complexity for everyday users. -### Solution +### Images image From 487776e52f7797a86112c4826080daf24f15f587 Mon Sep 17 00:00:00 2001 From: Cindy Cieniek <122636657+cindytrang@users.noreply.github.com> Date: Sun, 13 Oct 2024 10:52:46 +0100 Subject: [PATCH 22/34] Update README.md --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 78e69a7..a7f4b3f 100644 --- a/README.md +++ b/README.md @@ -70,10 +70,10 @@ The app’s streamlined process allows users to split expenses and execute payme ## Features - Payment Splitting (By Full, By Item, Evenly) -- Lending and Borrowing Opportunity -- Creating groups -- Receipt OCR system -- Simple and intuitive UI thanks to Passkeys integration +- Lending and Borrowing Compatibility +- Creating Groups/Expenses +- Receipt OCR system to extract the specific costs +- Simple and intuitive UI thanks to Passkeys Integration ## Tech Stack From be47b394b07f42d33c75286ec3df9c449e37fa96 Mon Sep 17 00:00:00 2001 From: Spoyte <104215259+Spoyte@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:03:19 +0100 Subject: [PATCH 23/34] update invoker auth --- contracts/StellarPay2.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/StellarPay2.rs b/contracts/StellarPay2.rs index e7da138..4f2570f 100644 --- a/contracts/StellarPay2.rs +++ b/contracts/StellarPay2.rs @@ -37,6 +37,7 @@ pub struct Contract; impl Contract { // Member functions pub fn set_member(env: Env, address: Address, nickname: String) { + address.require_auth(); let member = Member { nickname, address: address.clone() }; env.storage().instance().set(&DataKey::Member(address), &member); } From 5fd2f2db376873b7a0e02268aa37ab39fb360636 Mon Sep 17 00:00:00 2001 From: Cindy Cieniek <122636657+cindytrang@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:40:52 +0100 Subject: [PATCH 24/34] Update README.md From 41407dbeab3347bf9ac9694991e2926478483c20 Mon Sep 17 00:00:00 2001 From: Cindy Cieniek <122636657+cindytrang@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:45:18 +0100 Subject: [PATCH 25/34] Update README.md Live demo added --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a7f4b3f..653fb60 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ graph TD ### Links +- [Live Demo](https://splittertab.vercel.app/) - [Demo](https://www.youtube.com/watch?v=EyXrEwHLkoo) - [Pitch Deck](https://www.canva.com/design/DAGTabHuIc4/BDLopYPbazWElCHx-Q-Z7A/edit?utm_content=DAGTabHuIc4&utm_campaign=designshare&utm_medium=link2&utm_source=sharebutton ) From 2b6425f5721025298f626a2d0910f52647be8931 Mon Sep 17 00:00:00 2001 From: Cindy Cieniek <122636657+cindytrang@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:47:07 +0100 Subject: [PATCH 26/34] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 653fb60..baa31e9 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,12 @@ The app’s streamlined process allows users to split expenses and execute payme - **Web3** - Stellar + Soroban smart contracts - **Auth** - Passkeys / Freigther + ## Tech Team + +- **DANIEL ZARZECKI ** - daniel.zarzecki047@gmail.com | @nevvdevv +- **NOÉ DE LARMINAT** - noe.de.91@gmail.com | @NodeLarminat +- **CINDY CIENIEK** - cindycieniek@gmail.com | @ciencck28593 + ## Getting Started - Install dependenciex - `pnpm i` From f7a5390cdee914b66c1018d02828b97c61b64217 Mon Sep 17 00:00:00 2001 From: Cindy Cieniek <122636657+cindytrang@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:47:23 +0100 Subject: [PATCH 27/34] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index baa31e9..c12d7a2 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ The app’s streamlined process allows users to split expenses and execute payme ## Tech Team -- **DANIEL ZARZECKI ** - daniel.zarzecki047@gmail.com | @nevvdevv +- **DANIEL ZARZECKI** - daniel.zarzecki047@gmail.com | @nevvdevv - **NOÉ DE LARMINAT** - noe.de.91@gmail.com | @NodeLarminat - **CINDY CIENIEK** - cindycieniek@gmail.com | @ciencck28593 From e19b9e70a01d5c78c3476b077bbc968b6bb8c9c2 Mon Sep 17 00:00:00 2001 From: Spoyte <104215259+Spoyte@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:48:38 +0100 Subject: [PATCH 28/34] Update StellarPay2.rs --- contracts/StellarPay2.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/contracts/StellarPay2.rs b/contracts/StellarPay2.rs index 4f2570f..074e9e3 100644 --- a/contracts/StellarPay2.rs +++ b/contracts/StellarPay2.rs @@ -1,11 +1,12 @@ #![no_std] -use soroban_sdk::{contract, contracttype, contractimpl, Address, Env, Map, String, Symbol, Vec, Error}; +use soroban_sdk::{contract, contracttype, contractimpl, Address, Env, Map, String, Symbol, Vec, Error, log}; #[contracttype] pub enum DataKey { Member(Address), Group(Symbol), Transaction(Address), // Modified to only use Address + AllGroupIds, // New key for storing the list of all group IDs } #[contracttype] @@ -49,7 +50,26 @@ impl Contract { // Group functions pub fn set_group(env: Env, group_id: Symbol, owner: Address, members: Map) { let group = Group { group_id: group_id.clone(), owner, members }; - env.storage().instance().set(&DataKey::Group(group_id), &group); + env.storage().instance().set(&DataKey::Group(group_id.clone()), &group); + + // Update the list of all group IDs + let mut all_group_ids: Vec = env.storage().instance().get(&DataKey::AllGroupIds).unwrap_or_else(|| { + log!(&env, "Initializing new all_group_ids vector"); + Vec::new(&env) + }); + + if !all_group_ids.contains(&group_id) { + log!(&env, "Adding new group_id to all_group_ids: {:?}", group_id); + all_group_ids.push_back(group_id.clone()); + env.storage().instance().set(&DataKey::AllGroupIds, &all_group_ids); + } + + log!(&env, "Current all_group_ids: {:?}", all_group_ids); + } + pub fn get_all_group_ids(env: Env) -> Vec { + let all_group_ids = env.storage().instance().get(&DataKey::AllGroupIds).unwrap_or_else(|| Vec::new(&env)); + log!(&env, "Retrieved all_group_ids: {:?}", all_group_ids); + all_group_ids } pub fn get_group(env: Env, group_id: Symbol) -> Result { From 791cfca56c6a94693b2ee00d6f1a3d66ef0e4598 Mon Sep 17 00:00:00 2001 From: JustAnotherDevv Date: Sun, 13 Oct 2024 11:48:40 +0100 Subject: [PATCH 29/34] fix --- dapp/index.html | 2 +- dapp/package.json | 2 + dapp/pnpm-lock.yaml | 64 ++++++ dapp/src/components/Layout.tsx | 2 +- dapp/src/components/ui/dialog.tsx | 120 +++++++++++ dapp/src/components/ui/progress.tsx | 26 +++ dapp/src/hooks/use-auth.tsx | 14 +- dapp/src/pages/Landing.tsx | 22 +- dapp/src/pages/Profile.tsx | 304 ++++++++++++++++++---------- 9 files changed, 424 insertions(+), 132 deletions(-) create mode 100644 dapp/src/components/ui/dialog.tsx create mode 100644 dapp/src/components/ui/progress.tsx diff --git a/dapp/index.html b/dapp/index.html index 6857097..b6c7f3a 100644 --- a/dapp/index.html +++ b/dapp/index.html @@ -4,7 +4,7 @@ - SplitPay + SplitterTab
diff --git a/dapp/package.json b/dapp/package.json index 9a86239..46e2a7d 100644 --- a/dapp/package.json +++ b/dapp/package.json @@ -16,8 +16,10 @@ "@darkedges/capacitor-native-webauthn": "^0.0.4", "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-radio-group": "^1.2.1", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-slot": "^1.1.0", diff --git a/dapp/pnpm-lock.yaml b/dapp/pnpm-lock.yaml index 8ff0b72..e94714b 100644 --- a/dapp/pnpm-lock.yaml +++ b/dapp/pnpm-lock.yaml @@ -26,12 +26,18 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.3.1) '@radix-ui/react-label': specifier: ^2.1.0 version: 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-progress': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-radio-group': specifier: ^1.2.1 version: 1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -757,6 +763,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-dialog@1.1.2': + resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-direction@1.1.0': resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} peerDependencies: @@ -880,6 +899,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-progress@1.1.0': + resolution: {integrity: sha512-aSzvnYpP725CROcxAOEBVZZSIQVQdHgBr2QQFKySsaD14u8dNT0batuXI+AAGDdAHfXH8rbnHmjYFqVJ21KkRg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-radio-group@1.2.1': resolution: {integrity: sha512-kdbv54g4vfRjja9DNWPMxKvXblzqbpEC8kspEkZ6dVP7kQksGCn+iZHkcCz2nb00+lPdRvxrqy4WrvvV1cNqrQ==} peerDependencies: @@ -3208,6 +3240,28 @@ snapshots: optionalDependencies: '@types/react': 18.3.11 + '@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.11)(react@18.3.1) + aria-hidden: 1.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.6.0(@types/react@18.3.11)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.0 + '@radix-ui/react-direction@1.1.0(@types/react@18.3.11)(react@18.3.1)': dependencies: react: 18.3.1 @@ -3311,6 +3365,16 @@ snapshots: '@types/react': 18.3.11 '@types/react-dom': 18.3.0 + '@radix-ui/react-progress@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-context': 1.1.0(@types/react@18.3.11)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.11 + '@types/react-dom': 18.3.0 + '@radix-ui/react-radio-group@1.2.1(@types/react-dom@18.3.0)(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@radix-ui/primitive': 1.1.0 diff --git a/dapp/src/components/Layout.tsx b/dapp/src/components/Layout.tsx index ec955d9..5686cb8 100644 --- a/dapp/src/components/Layout.tsx +++ b/dapp/src/components/Layout.tsx @@ -16,7 +16,7 @@ const Logo = () => ( src="/logo.png" alt="" /> -

SplitPay

+

SplitterTab

); diff --git a/dapp/src/components/ui/dialog.tsx b/dapp/src/components/ui/dialog.tsx new file mode 100644 index 0000000..5d16351 --- /dev/null +++ b/dapp/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/dapp/src/components/ui/progress.tsx b/dapp/src/components/ui/progress.tsx new file mode 100644 index 0000000..3fd47ad --- /dev/null +++ b/dapp/src/components/ui/progress.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import * as ProgressPrimitive from "@radix-ui/react-progress" + +import { cn } from "@/lib/utils" + +const Progress = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, value, ...props }, ref) => ( + + + +)) +Progress.displayName = ProgressPrimitive.Root.displayName + +export { Progress } diff --git a/dapp/src/hooks/use-auth.tsx b/dapp/src/hooks/use-auth.tsx index 294b5f0..2d1aa17 100644 --- a/dapp/src/hooks/use-auth.tsx +++ b/dapp/src/hooks/use-auth.tsx @@ -32,7 +32,6 @@ interface AuthContextType { const AuthContext = createContext(undefined); -// Import environment variables const PUBLIC_horizonUrl = import.meta.env.VITE_PUBLIC_horizonUrl; export const AuthProvider: React.FC<{ children: ReactNode }> = ({ @@ -60,7 +59,6 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ setUserAddress(newBundlerKey.publicKey()); localStorage.setItem("sp:bundler", newBundlerKey.secret()); - // Fund the new account using friendbot const horizon = new Horizon.Server(PUBLIC_horizonUrl!); try { await horizon.friendbot(newBundlerKey.publicKey()).call(); @@ -74,12 +72,11 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ useEffect(() => { initializeBundler(); - // Check if user is already authenticated const storedDeployee = localStorage.getItem("sp:deployee"); if (storedDeployee) { setDeployee(storedDeployee); setIsAuthenticated(true); - setConnection({ status: "connected", network: "testnet" }); // Assuming testnet, adjust as needed + setConnection({ status: "connected", network: "testnet" }); } }, [initializeBundler]); @@ -125,7 +122,7 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ setDeployee(newDeployee); localStorage.setItem("sp:deployee", newDeployee); setIsAuthenticated(true); - setConnection({ status: "connected", network: "testnet" }); // Assuming testnet, adjust as needed + setConnection({ status: "connected", network: "testnet" }); } else { throw new Error("BundlerKey is not initialized"); } @@ -145,11 +142,12 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ userVerification: "discouraged", }); - // Here you might want to verify the authentication on your server - // For now, we'll just set the authenticated state localStorage.setItem("sp:id", signInRes.id); + + // + setIsAuthenticated(true); - setConnection({ status: "connected", network: "testnet" }); // Assuming testnet, adjust as needed + setConnection({ status: "connected", network: "testnet" }); } catch (error) { console.error("Sign in error:", error); throw error; diff --git a/dapp/src/pages/Landing.tsx b/dapp/src/pages/Landing.tsx index 2b2ad6f..ee7226a 100644 --- a/dapp/src/pages/Landing.tsx +++ b/dapp/src/pages/Landing.tsx @@ -81,26 +81,11 @@ const Landing = () => {

- CryptoSplit + SplitterTab

Split expenses with crypto, revolutionize your finances

- {/*
- - -
*/} @@ -164,12 +149,12 @@ const Landing = () => { - About CryptoSplit + About SplitterTab

- CryptoSplit is the ultimate solution for splitting expenses + SplitterTab is the ultimate solution for splitting expenses using cryptocurrencies. Whether you're traveling with friends, sharing a house, or managing group purchases, CryptoSplit makes it easy to keep track of expenses and settle debts using your @@ -215,7 +200,6 @@ const Landing = () => { {feature.description}

- {/* */} diff --git a/dapp/src/pages/Profile.tsx b/dapp/src/pages/Profile.tsx index cf05f2e..7944387 100644 --- a/dapp/src/pages/Profile.tsx +++ b/dapp/src/pages/Profile.tsx @@ -3,6 +3,16 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { motion, AnimatePresence } from "framer-motion"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { Progress } from "@/components/ui/progress"; +import { Check, X } from "lucide-react"; type Group = { id: number; @@ -12,10 +22,25 @@ type Group = { isCreator: boolean; }; +interface Wallet { + id: number; + user: string; + amount: number; + completed: boolean; +} + +interface ExpenseDetails { + id: number; + organizer: string; + totalAmount: number; + wallets: Wallet[]; +} + const MotionCard = motion(Card); const MotionButton = motion(Button); export default function Profile() { + const [isOpen, setIsOpen] = useState(false); const [groups, setGroups] = useState([ { id: 1, @@ -34,6 +59,25 @@ export default function Profile() { ]); const { toast } = useToast(); + const expenseDetails: ExpenseDetails = { + id: 1, + organizer: "John Doe", + totalAmount: 1000, + wallets: [ + { id: 1, user: "Alice", amount: 250, completed: true }, + { id: 2, user: "Bob", amount: 250, completed: false }, + { id: 3, user: "Charlie", amount: 250, completed: true }, + { id: 4, user: "David", amount: 250, completed: false }, + ], + }; + + const totalPaid = expenseDetails.wallets.reduce( + (sum, wallet) => (wallet.completed ? sum + wallet.amount : sum), + 0 + ); + + const progressPercentage = (totalPaid / expenseDetails.totalAmount) * 100; + const handlePayment = (groupId: number) => { setGroups( groups.map((group) => { @@ -65,52 +109,101 @@ export default function Profile() { animate={{ opacity: 1 }} transition={{ duration: 0.5 }} > -
+ + {/* +
+ Click to view expense details +
+
*/} + + + + Expense Details + + Organized by {expenseDetails.organizer} + + +
+

Wallets

+
    + {expenseDetails.wallets.map((wallet) => ( +
  • + + {wallet.user}: ${wallet.amount} + + {wallet.completed ? ( + + ) : ( + + )} +
  • + ))} +
+
+
+

Payment Progress

+ +

+ ${totalPaid} paid of ${expenseDetails.totalAmount} total +

+
+ +
+
- -
- - Your Expense Groups - - - Where friends split bills and HODL together! 🚀💰 - + > +
+ +
+ + Your Expense Groups + + + Where friends split bills and HODL together! 🚀💰 + +
-
- {/* Your Expense Groups */} - - {groups.map((group) => ( - - - {group.name} - - - - Total Amount: ${group.totalAmount} - Paid Amount: ${group.paidAmount} - -
+ + {groups.map((group) => ( + + + {group.name} + + -
- {group.isCreator ? ( - handleWithdraw() - // group.id - } - className="mt-4 w-full text-gray-700" - disabled={group.paidAmount < group.totalAmount} - variant="outline" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - > - Withdraw Funds - - ) : ( - handlePayment(group.id)} - className="mt-4 w-full text-gray-700" - disabled={group.paidAmount === group.totalAmount} - variant="outline" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} + className="flex justify-between items-center mb-4" + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + transition={{ delay: 0.2, duration: 0.3 }} > - Pay Your Share - - )} -
-
- ))} -
+ Total Amount: ${group.totalAmount} + Paid Amount: ${group.paidAmount} +
+
+ +
+ +
+ {group.isCreator ? ( + handleWithdraw() + // group.id + } + className="mt-4 w-full text-gray-700" + disabled={group.paidAmount < group.totalAmount} + variant="outline" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Withdraw Funds + + ) : ( + handlePayment(group.id)} + className="mt-4 w-full text-gray-700" + disabled={group.paidAmount === group.totalAmount} + variant="outline" + whileHover={{ scale: 1.05 }} + whileTap={{ scale: 0.95 }} + > + Pay Your Share + + )} +
+
+ + + ))} + +
); } From 6b0dad52da8d3ef8b1ab3491878cf4a609aefc3c Mon Sep 17 00:00:00 2001 From: Cindy Cieniek <122636657+cindytrang@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:54:04 +0100 Subject: [PATCH 30/34] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c12d7a2..f3625aa 100644 --- a/README.md +++ b/README.md @@ -62,12 +62,17 @@ The app’s streamlined process allows users to split expenses and execute payme ### Images +Screenshot 2024-10-13 at 11 50 51 + image image image +Screenshot 2024-10-13 at 11 53 27 + + ## Features - Payment Splitting (By Full, By Item, Evenly) From 02fa1ff560e6fba9cc8658e6ae26468421c28b6b Mon Sep 17 00:00:00 2001 From: Spoyte <104215259+Spoyte@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:56:19 +0100 Subject: [PATCH 31/34] Update README --- contracts/README | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/contracts/README b/contracts/README index 40469fe..fb9aa8d 100644 --- a/contracts/README +++ b/contracts/README @@ -20,3 +20,12 @@ stellar network add \ --global testnet \ --rpc-url https://soroban-testnet.stellar.org:443 \ --network-passphrase "Test SDF Network ; September 2015" + + +Contracts deployed: +Testnet: + Upload: https://futurenet.stellarchain.io/transactions/56678a618ce44df498de9a2e433ce9b2ca310a8fff7d2ca4ae0493964c1dd36e + Create: https://futurenet.stellarchain.io/transactions/415a8ec3f1d16425f4ecfc1539749b6c9c2ea564259adaacc2f04942aa1c9ca3 +Futurnet: + Upload: https://testnet.stellarchain.io/transactions/0c429f3f64a4a0a6064b7c231e6078a9c0b0a70d5e272970543ccf24b56e945d + Create: https://testnet.stellarchain.io/transactions/ef96c6e47417ecec40743ccf7c4f5b383afa42e09ee1c3689d97997bcd630025 From e4e02283adf2c7c467179ccb11f300f0ef128c54 Mon Sep 17 00:00:00 2001 From: NevvDevv <61601037+JustAnotherDevv@users.noreply.github.com> Date: Sun, 13 Oct 2024 13:01:59 +0200 Subject: [PATCH 32/34] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f3625aa..03bf595 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ graph TD ### Links - [Live Demo](https://splittertab.vercel.app/) -- [Demo](https://www.youtube.com/watch?v=EyXrEwHLkoo) +- [Demo](https://www.loom.com/share/930ee89b5d7a4f12b8a0923dd61c431c?sid=c239ff8f-8ca2-4d30-b24b-99ee91baf506)) - [Pitch Deck](https://www.canva.com/design/DAGTabHuIc4/BDLopYPbazWElCHx-Q-Z7A/edit?utm_content=DAGTabHuIc4&utm_campaign=designshare&utm_medium=link2&utm_source=sharebutton ) From 880bdaefe4a957eb0b5310256a3a7b55652f42ef Mon Sep 17 00:00:00 2001 From: NevvDevv <61601037+JustAnotherDevv@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:09:50 +0200 Subject: [PATCH 33/34] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 03bf595..8fa6b18 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ The app’s streamlined process allows users to split expenses and execute payme ## Tech Team -- **DANIEL ZARZECKI** - daniel.zarzecki047@gmail.com | @nevvdevv +- @nevvdevv - **NOÉ DE LARMINAT** - noe.de.91@gmail.com | @NodeLarminat - **CINDY CIENIEK** - cindycieniek@gmail.com | @ciencck28593 From 9f3e50391ecb3a1f626d14dae88fc0530baa27d2 Mon Sep 17 00:00:00 2001 From: NevvDevv <61601037+JustAnotherDevv@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:30:15 +0200 Subject: [PATCH 34/34] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fa6b18..db3366e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## Payment Splitter +## Splitter Tab