Giter Site home page Giter Site logo

ptrajectory / payments Goto Github PK

View Code? Open in Web Editor NEW
8.0 0.0 0.0 25.02 MB

A mobile money payment intergration sdk.

Home Page: https://payments.ptrajectory.com

TypeScript 98.22% JavaScript 0.64% HTML 0.07% CSS 1.07%
indiedev mpesa-sdk sdk-typescript

payments's Introduction

payments's People

Contributors

porkytheblack avatar this-isdon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

payments's Issues

implement auth

Changes made in pr #17

Supabase Client initialization

reason

  • also has self hosting capabilities
    const client = createClient(the_hosted_url, anonymous_key) 
    client.from("quotes").select()
    this.rest = new PostgrestClient(`${_supabaseUrl}/rest/v1`, { // ๐Ÿ‘ˆ your url's getting passed in here
      headers: this.headers, // ๐Ÿ‘ˆ includes your supabase key as an Authorization header
      schema: settings.db?.schema,
      fetch: this.fetch,
    })

My Initialization

const client = new PaymentsClient(your_url, your_secret_or_publishable_key) 
export const createPaymentClient = (url: string, secretKey:string ) => {

  if(isUndefined(url) || url?.length === 0) throw new Error("INVALID URL")

  if(isUndefined(secretKey) || secretKey?.length === 0) throw new Error("INVALID URL")

  const client = new PaymentsHttpClient()

  client.url = url 

  client.secretKey = secretKey

  return {
      /**
       * Interact with payments
       */
      payments: new Payment(client),
      /**
       * Interact with products
       */
       products: new Product(client),
       /**
        * Interact with customers
        */
       customers: new Customer(client),
      /**
       * Interact with payment methods
       */
      paymentMethods: new PaymentMethod(client),
      /**
       * Interact with carts
       */
      carts: new Cart(client),
      /**
       * Interact with checkouts
       */
      checkouts: new Checkout(client)
  }
}

How my auth works

  • Hierachical structure of entities, where all sub entities are dependant on their parents.

image

  • All sub entities beneath the store will have store_id, linking them back to the store.
  • Auth is done per store, i.e when initializing a paymentClient, you use credentials that point to a specific store.
  • This is achieved by using jwts with the store data encoded into them

image
  • e.g ::
const A_TEST_KEY = "test_eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiVEVTVElORyBTVE9SRSIsImlkIjoic3RyX2Y0ODQzNTg1NGFiODYzZTBmZWQ5MTZlZmU5OTdkMmFmIiwic2VsbGVyX2lkIjoic2xsX2UwMzRlN2UxYWYwMDFiYTdmNjAxYjJlZWY4ZmE1OWZkIiwiZW52IjoidGVzdGluZyIsImlhdCI6MTY5NDA5OTc5NX0.UBodqZrttDIRUW3TXObwT6jwsFP1ns9iDsPvxGXXq0Y"

image


  • any entity created with this key, will be created under the specific store.

Two different kinds of keys

  • there are 2 different kinds of keys i.e secret_key(only to be used server side) and publishable key (can be used client side)

  • These two are generated and signed differently

Publishable

  • On application setup, i.e before running the application for the first time, a public-private rsa key pair is generated
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
        modulusLength: 4096,
        publicKeyEncoding: {
            type: 'spki',
            format: 'pem'
        },
        privateKeyEncoding: {
            type: 'pkcs8',
            format: 'pem'
        }
    })
  • The private key gets used when signing/encoding the jwt
export function generatePublishableKey(store: STORE, env: environment){
    const privateKey = fs.readFileSync("./keys/privateKey.pem", {
        encoding: 'utf-8'
    })
    const publishableKey = jwt.sign({
        ...store,
        env
    }, privateKey, {
        algorithm: 'RS256'
    })

    return publishableKey

}
  • The public key on validation
export function verifyPublishableKey(token: string): Promise<STORE & { env: environment } >{
    
    const publicKey = fs.readFileSync("./keys/publicKey.pem",{
        encoding: 'utf-8'
    })

    return new Promise((res, rej)=>{
        jwt.verify(token, publicKey, (err, store)=>{
            
            if(err) return rej("Unable to verify")

            return res(store as any)
        }) 

    })

}

Secret Key

  • a random base64 string is used to sign the jwt, this string is generated as shown below
randomBytes(32).toString('base64')
  • used for signing and verifying

  • Middleware is used to control access to the resources


image

Ephemeral keys

  • Inorder to support payments being triggered by the client, I have adapted the concept of single use ephemeral keys, which can only be used in combination with a checkout object. They are meant to make the checkout process secure.
  • Are signed using the same key as the publishable key
export const generateCheckoutEphemeralKey = (checkout_id: string) => {

    const privateKey = fs.readFileSync("./keys/privateKey.pem", {
        encoding: 'utf-8'
    })
    const publishableKey = jwt.sign({
        checkout_id
    }, privateKey, {
        algorithm: 'RS256'
    })

    return publishableKey

}
  • and include the checkout id in the payload.

Support for environments


image
  • Similar to stripe
  • Support for different environments is crucial to enable testing before production, especially on the payments side.
  • In addition to the store data, all the jwts include the store environment.
  • environments also help control access to different resources

Breaking the dashboard app

  • New changes introduces breaking changes to the dashboard app

Migrate transactions app to app router

Updated file structure

pages -> app
introduction of new file naming conventions under the app directory:
FILE CONVENTIONS

  • page

Screenshot 2023-09-05 at 21 09 17
  • layout
  • loading
  • error
  • route

image

within the dir you can create sub components of the folder which can allow you to put any page sections within the same directory as the page.

  • better file organization

image
  • using a single layout file reduces the complexity of configuring layouts
  • error and loading status pages are simple

Data fetching patterns

CACHING DOCS

  • caching

    • requests made with fetch are automatically cached, but there are a number of caching strategies to choose from
          // This will be automatically cached if used in a server component / route handler / server action
           const data = fetch("https://kanye.rest")   
    • caching db queries
        const data = cache(await db.query.CUSTOMERS.findMany())
  • server component

      "use server"
    
      const getQuote = async () => {
          const q =  await db.QUOTES.findMand()
          return q
      }
    
      const MyComponent = async () => {
         
           const quote = await getQuote()
           return (
                  <h1>
                       { quoute.title }
                  <h2>
            )
      }
  • route handlers
    initially you would export a single handler function from
    defining different handlers for different http verbs separately


export const GET = async  (request: Request,
    params: { customer_slug?: string[] }
    ) => {
    
    const { customer_slug } = params

    const { userId, user } = auth()

    if(isEmpty(userId) || isUndefined(userId)) return NextResponse.json(generate_dto(null, "UNAUTHORIZED", 'error'), {
        status: 401
    })


  ....some_hidden_code

}

  • server actions
    SERVER ACTIONS
    • experimental ๐Ÿ˜…
    • comes with a warning
    • send data to the server and get a reponse back

image

"use server"


export const fetch_home_daily_purchases_chart_data= async (store_id: string, range: Partial<{from: string, to: string}> | undefined ): Promise<Array<ChartData>>  => {

    try {

        // vvalidate the start and end dates
        const { from = get_today_start().toISOString(), to = get_today_end().toISOString() } = range ? range : {}
    
        // get all the payments between those dates
    
        let all_payments: Array<tPAYMENT & { created_at?: string }> | null = (await db.select().from(PAYMENT).where(
            and(
                gt(PAYMENT.created_at, new Date(from)),
                lt(PAYMENT.created_at, new Date(to))
            ),
        )) ?? null
    
        const date_categories = getAllDaysBetweenDates(from, to)
    
        // transform the data into a format that can go into the chart
    
        const date_categorized_chart_data = date_categories?.val?.map((date)=>{
    
            const payment_aggregation = all_payments?.reduce((prev, cur, i)=>{
    
                if(date_categories?.is_same_date) {
                    if(isSameHour(date, cur.created_at ?? new Date())) return prev + (cur?.amount ?? 0)
    
                    return prev + 0
                }
    
                if(isSameDate(date, cur.created_at ?? new Date())) return prev + (cur?.amount ?? 0)
    
                return prev + 0
            }, 0) ?? 0
    
    
            return {
                day: dayjs(date).format(
                    date_categories?.is_same_date ? "hh A" :"MMM D"
                ),
                total_amount_spent: payment_aggregation
            }
    
        })
    
        return date_categorized_chart_data
    }
    catch (e)
    {
        console.log("SOMETHING WENT WRONG::", e)
        //TODO: add better error handling
        return []
    }

}

  • make it easier to pass data around components, without having to use complex setups of global state managers.

Issues I faced

  • form data and file uploads
    • previously relying on formidable to parse the data for me before sending it up to cloudinary for storage
    • had to refactor, due to changes to the request object

       const POST = async (request: Request) {
             
             const formData = await request.formData()
             
            ....other interesting stuff
       }
 ```
--------------------

--------------------
- importing server component in a client component won't work but you can pass a server component as a prop to a client component

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.