Category Archives: Uncategorized

GraphQL server with Gqlgen and PostgreSQL

 

 

Create graphQL server using gqlgen following gqlgen tutorial

mkdir gqlgen-users
cd gqlgen-users
go mod init github.com/[username]/gqlgen-users

Create tools.go with gqlgen library imported.

tools.go

//go:build tools
// +build tools

package tools

import (
    _ "github.com/99designs/gqlgen"
)

Install packages

go mod tidy

Create the project skeleton

go run github.com/99designs/gqlgen init

 

Create database connector

databaseConnector/databaseConnector.go

package databaseConnector

import (
    "fmt"

    "github.com/jackc/pgtype"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type User struct {
    ID       uint   `gorm:"primaryKey"`
    Username string `gorm:"unique"`
    Email    string
    Age      int
    MetaData pgtype.JSONB `gorm:"type:jsonb" json:"fieldnameofjsonb"`
}

func autoMigrateDB(db *gorm.DB) {
    // Perform database migration
    err := db.AutoMigrate(&User{})
    if err != nil {
        fmt.Println(err)
    }
}

func connectToPostgreSQL() (*gorm.DB, error) {
    // dsn := "user=mynews password=test123 dbname=tests host=localhost port=5432 sslmode=disable"
    dsn := "user=toninichev dbname=tests host=localhost port=5432 sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        return nil, err
    }

    return db, nil
}

func createuserWithMetaData(db *gorm.DB, username string, email string, age int, metaData string) (*User, error) {
    jsonData := pgtype.JSONB{}
    err := jsonData.Set([]byte(metaData))
    if err != nil {
        return nil, err
    }
    // Create a user
    newUser := User{Username: username, Email: email, Age: age, MetaData: jsonData}
    err = createUser(db, &newUser)
    if err != nil {
        return nil, err
    }
    return &newUser, nil
}
func createUser(db *gorm.DB, user *User) error {
    result := db.Create(user)
    if result.Error != nil {
        return result.Error
    }
    return nil
}

func CreateDB(tableName string) error {
    db, err := connectToPostgreSQL()
    if err != nil {
        return err
    }
    autoMigrateDB(db)
    return nil
}

func CreateUser(username string, email string, age int, metaData string) (*User, error) {
    db, err := connectToPostgreSQL()
    if err != nil {
        return nil, err
    }
    user, err := createuserWithMetaData(db, username, email, age, metaData)
    return user, err
}

func GetUserByID(userID uint) (*User, error) {
    db, err := connectToPostgreSQL()
    if err != nil {
        return nil, err
    }

    var user User
    result := db.First(&user, userID)
    if result.Error != nil {
        return nil, result.Error
    }
    return &user, nil
}

func GetUserByMetaData(metaDataFilter string) (*User, error) {
    db, err := connectToPostgreSQL()
    if err != nil {
        return nil, err
    }

    var user User
    // result := db.First(&user, userID)

    result := db.Where(metaDataFilter).First(&user)

    if result.Error != nil {
        return nil, result.Error
    }
    return &user, nil
}

 

Edit schema adding the new Customer type, queries and mutations to retrieve and create new users.

graph/schema.graphqls

# GraphQL schema example
#
# https://gqlgen.com/getting-started/


type Customer {
  customerId: String!
  username: String!
  email: String!,
  age: Int!
  metaData: String!
}

input NewCustomer {
  customerId: String!
  username: String!
  email: String!,
  age: Int!
  metaData: String!
}

type Query {
  getCustomer(customerId: String!): Customer!
  getCustomerByMetaData(metaData: String!): Customer!
}

type Mutation {
  saveCustomer(input: NewCustomer!):Boolean!
  createDB(tableName: String!):Boolean!
}

Re-generate resolvers with the new schema

go run github.com/99designs/gqlgen generate

Implement the resolvers

graph/schema.resolvers.go

package graph

// This file will be automatically regenerated based on the schema, any resolver implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.44

import (
    "context"
    "strconv"
    "tutorials/gqlgen-users/databaseConnector"
    "tutorials/gqlgen-users/graph/model"
)

// SaveCustomer is the resolver for the saveCustomer field.
func (r *mutationResolver) SaveCustomer(ctx context.Context, input model.NewCustomer) (bool, error) {
    databaseConnector.CreateUser(input.Username, input.Email, input.Age, input.MetaData)
    return true, nil
}

// CreateDb is the resolver for the createDB field.
func (r *mutationResolver) CreateDb(ctx context.Context, tableName string) (bool, error) {
    err := databaseConnector.CreateDB(tableName)

    if err != nil {
        // handle error
        return false, err
    }
    return true, nil
}

// GetCustomer is the resolver for the getCustomer field.
func (r *queryResolver) GetCustomer(ctx context.Context, customerID string) (*model.Customer, error) {
    cid, _ := strconv.Atoi(customerID)
    var customer *databaseConnector.User
    var err error
    customer, err = databaseConnector.GetUserByID(uint(cid))

    if err != nil {
        // handle error
        return nil, err
    }

    // get the underlying byte slice.
    jsonbText, _ := customer.MetaData.Value()
    // Convert byte slice to string
    jsonString := string(jsonbText.([]byte))

    // map returned customer structure from the DB into the model
    c := model.Customer{
        CustomerID: strconv.FormatUint(uint64(customer.ID), 10),
        Username:   customer.Username,
        Email:      customer.Email,
        Age:        customer.Age,
        MetaData:   jsonString,
    }

    return &c, nil
}

// GetCustomerByMetaData is the resolver for the getCustomerByMetaData field.
func (r *queryResolver) GetCustomerByMetaData(ctx context.Context, metaData string) (*model.Customer, error) {
    customer, err := databaseConnector.GetUserByMetaData(metaData)

    if err != nil {
        // handle error
        return nil, err
    }

    // get the underlying byte slice.
    jsonbText, _ := customer.MetaData.Value()
    // Convert byte slice to string
    jsonString := string(jsonbText.([]byte))

    // map returned customer structure from the DB into the model
    c := model.Customer{
        CustomerID: strconv.FormatUint(uint64(customer.ID), 10),
        Username:   customer.Username,
        Email:      customer.Email,
        Age:        customer.Age,
        MetaData:   jsonString,
    }

    return &c, nil
}

// Mutation returns MutationResolver implementation.
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }

// Query returns QueryResolver implementation.
func (r *Resolver) Query() QueryResolver { return &queryResolver{r} }

type mutationResolver struct{ *Resolver }
type queryResolver struct{ *Resolver }

 

 

In-app purchase with server to server notifications

 GitHub Repo

 

  • Contains these projects:
    • iOS-App (The iOS app that could be used to test the in-app purchase)
    • services-key-generator (a helper app that generates JWT for App Store content and server APIs)
    • app-store-server-library (Apple demo app that could be used to trigger various events in server to server notifications)
    • backend-server (the actual server, that will receive server-2-server notifications from Apple App Store server)

Creating application bundle in App Store connect

  1. Click + in `https://appstoreconnect.apple.com/apps`
  2. Create bundle identifier
    refer to Apple documentation for more details
  3. on ‘Platforms’ select iOS
  4. Add bundle identifier name (preferably following this pattern (com.MyCompany.appName)

Create new Application in App Store Connect

https://appstoreconnect.apple.com/apps

  1. Select New App
  2. Add the same app bundle that you created.
  3. Make sure that on `platforms` you select iOS

Create products in App Store connect.

Once you create application, you have to create some products to sell.

In general there are these types of products supported by Apple’s App Store:

  • Consumables.
    • Consumables are products that can be used (‘consumed’) once and then repurchased multiple times.
  • Non-consumables
    • Non-consumables are products that are purchased once, have no expiry date, and remain permanently available within your app.
  • Auto-renewal subscriptions
    • Auto-renewal subscriptions are products or services that you pay for on a recurring basis. Developers love these, as they guarantee a steady income stream.
  • Non auto-renewal subscriptions.
    • Non auto-renewal subscriptions are those that run for a fixed period of time, after which you can choose to manually renew. Often, these last longer and cost more than auto-renewal subscriptions.

To create products navigate to the newly created app, and locate “MONETIZATION” on the left side. From there you could select to create “in-app-purchases” or “Subscriptions

In app purchase

Refer to Apple documentation of how to create products: In-App purchase

Creating Sandbox testing users

Ahh, yeah … you need test users so you won’t be charged with each test.

Refer to Apple documentation of how to create Sandbox test users

  • navigate to “users and access” -> sandbox
  • Click the “+” sign to create a new sandbox tester.

Set up your physical phone to use Sandbox test users

In order to test in-app purchase you have to deploy the app to the real device. Won’t work on the simulator.

In order to not be charged on every purchase test, you need to set up a Sandbox test users and set your physical iPhone to use them.

  1. Set up Sandbox test users (described above)
  2. On your physical phone navigate to Settings->App Store->SANDBOX ACCOUNT and add the new Sandbox user email there.

Make sure that you already enabled Developer mode on the iPhone

Creating the iOS app in X-Code

  1. Create new Xcode project and change the bundle id to the one that you set up above in the app store connect. (Refer to Creating application bundle in App Store connect in this article.)
  2. In-app purchase is not enabled by default. Let’s add it in Signing & Capabilities
  3. Add mainStore.storekit file by going to files->new and look for StoreKit2.

    – Name it mainStore.storekit
    – Make sure that you select ‘synced‘ so it will pull data from the actual App Store and select your apple bundle id (this is how the StoreKit2 will know which items to pull)4. Let’s add another Swift class which will be our shared Store.

store.swift

import StoreKit

@MainActor final class Store: ObservableObject {
    // use the same ids that you defined in the App Store connect
    private var productIDs = ["InAppPurchaseTutorialToniconsumableFuel", "InAppPurchaseTutorialToniConsumableOil", "BrakePads", "InAppPurchaseRenewalPro"]
    private var updates: Task<Void, Never>?
    
    @Published var products = [Product]()
    @Published var activeTransactions: Set<StoreKit.Transaction> = []
    
    init() {
        Task {
            await requestProducts()
        }
        Task {
            await transactionUpdates()
        }
    }
    
    deinit {
        updates?.cancel()
    }
    
    func transactionUpdates () async {
        for await update in StoreKit.Transaction.updates {
            if let transaction = try? update.payloadValue {
                activeTransactions.insert(transaction)
                await transaction.finish()
            }
            }
    }
    
    func requestProducts() async {
        do {
            products = try await Product.products(for: productIDs)
        } catch {
            print(error)
        }
    }
    
    func purchaseProduct(_ product: Product) async throws {
        print("Purchase tapped ...")
        let result = try await product.purchase()
        switch result {
        case .success(let verifyResult):
            print("Purchase successfull!")
            if let transaction = try? verifyResult.payloadValue {
                activeTransactions.insert(transaction)
                print("PURCHASE:")
                try print(verifyResult.payloadValue)
                print(transaction)
                await transaction.finish()
            }
        
        case .userCancelled:
            print("Purchase Canceled !")
            break
        case .pending:
            print("Purchase pending ...")
            break
        @unknown default:
            break
        }
    }
    
    func fetchActiveTransactions() async {
        var activeTransactions: Set<StoreKit.Transaction> = []
        for await entitelment in StoreKit.Transaction.currentEntitlements {
            if let transaction = try? entitelment.payloadValue {
                activeTransactions.insert(transaction)
                print("fetchActiveTransactions: ")
                print(transaction)
            }
        }
    }
}

In productIDs use the same ids that you defined in the App Store connect.

We define simple class that could be used in each View to retrieve purchases, make purchases, and update all views that is using it.
View updates happen since the class comforting to the ObservableObject protocol.

5. Edit the View to add purchase buttons.

Views/ContentView.swift

//
//  ContentView.swift
//  InAppPurchaseTutorial
//
//  Created by Toni Nichev on 1/2/24.
//

import SwiftUI
import StoreKit

struct ContentView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        

        VStack {
            Text("Welcome to my store").font(.title)
            ProductView(id: "InAppPurchaseTutorialToniconsumableFuel") {
                Image(systemName: "crown")
            }
            .productViewStyle(.compact)
            .padding()
            .onInAppPurchaseCompletion { product, result in
                if case .success(.success(let transaction)) = result {
                    print("Purchased successfully: \(transaction.signedDate)")
                } else {
                    print("Something else happened")
                }
            }
        }
        
        Section(header: Text("To buy:").font(.title)) {
            ForEach(store.products, id: \.id)  { product in
                Button {
                    Task {
                        try await store.purchaseProduct(product)
                    }
                } label: {
                    HStack {
                        Text(product.displayName + ":")
                        Text(verbatim: product.displayPrice)
                    }
                }
                .buttonStyle(.borderedProminent)
            }
        }
    }
}

#Preview {
    ContentView().environmentObject(Store())
}

6. Last part is to add method to retreive purchases when the application starts.

InAppPurchaseTutorialApp.swift

//
//  InAppPurchaseTutorialApp.swift
//  InAppPurchaseTutorial
//
//  Created by Toni Nichev on 1/2/24.
//

import SwiftUI

@main
struct InAppPurchaseTutorialApp: App {
    @Environment(\.scenePhase) private var sceneParse
    @StateObject private var store = Store()
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(store)
                .task(id: sceneParse) {
                    if sceneParse == .active {
                        await store.fetchActiveTransactions()
                    }
                }
        }
    }
}

Setting up Backend server to listen to server-2-server notifications from Apple server

Apple server sends server-to-server notifications in real time when subscription event happens, like subscribed, cancel subscription, did renew etc.

Mode details of all notification types could be found in Apple documentation

Purpose: we can receive these notifications on transaction complete, on purchase made, on subscriptions: purchased, canceled, failed and we could grant access to subscribers to some paid services like (pro news, premium game levels etc.)

  1. Create your backend server handler, that Apple App Store server will notify.
    I like to use PHP version cause it’s super easy to implement but any language works the same way:index.php

    <?php
    header('Status: 200');
    header("HTTP/1.1 200 OK"); 
    
    ini_set('display_errors', 1);
    error_reporting(E_ALL);
    
    
    // Download the certificate -> https://www.apple.com/certificateauthority/AppleRootCA-G3.cer
    // Convert it to .PEM file, run on macOS terminal ->  ```bash openssl x509 -in AppleRootCA-G3.cer -out apple_root.pem```
    
    $pem = file_get_contents('./assets/apple_root.pem');
    // $data = file_get_contents('./test.json'); // replace with file_get_contents('php://input');
    $data = file_get_contents('php://input'); // replace with file_get_contents('php://input');
    $json = json_decode($data);
    
    
    $header_payload_secret = explode('.', $json->signedPayload);
    
    //------------------------------------------
    // Header
    //------------------------------------------
    $header = json_decode(base64_decode($header_payload_secret[0]));
    
    $algorithm = $header->alg;
    $x5c = $header->x5c; // array
    $certificate = $x5c[0];
    $intermediate_certificate = $x5c[1];
    $root_certificate = $x5c[2];
    
    $certificate =
          "-----BEGIN CERTIFICATE-----\n"
        . $certificate
        . "\n-----END CERTIFICATE-----";
    
    $intermediate_certificate =
          "-----BEGIN CERTIFICATE-----\n"
        . $intermediate_certificate
        . "\n-----END CERTIFICATE-----";
    
    $root_certificate =
          "-----BEGIN CERTIFICATE-----\n"
        . $root_certificate
        . "\n-----END CERTIFICATE-----";
    
    //------------------------------------------
    // Verify the notification request   
    //------------------------------------------
    if (openssl_x509_verify($intermediate_certificate, $root_certificate) != 1){ 
        echo 'Intermediate and Root certificate do not match';
        exit;
    }
    
    // Verify again with Apple root certificate
    if (openssl_x509_verify($root_certificate, $pem) == 1){
        
        //------------------------------------------
        // Payload
        //------------------------------------------
        // https://developer.apple.com/documentation/appstoreservernotifications/notificationtype
        // https://developer.apple.com/documentation/appstoreservernotifications/subtype
    
        $payload = json_decode(base64_decode($header_payload_secret[1]));
        $notificationType = $payload->notificationType;
        $subtype = $payload->subtype;
    
        
    
        $transactionInfo = $payload->data->signedTransactionInfo;
        $ti = explode('.', $transactionInfo);
        
        $data = json_decode(base64_decode($ti[1]));
    
        // var_dump($payload); // this will contain our originalTransactionId
        file_put_contents("./data.txt", PHP_EOL . PHP_EOL . '=====================================' . PHP_EOL, FILE_APPEND);
        file_put_contents("./data.txt", print_r($payload, true),  FILE_APPEND);
        file_put_contents("./data.txt", '-------------------------------------' . PHP_EOL, FILE_APPEND);
        file_put_contents("./data.txt", print_r($data, true), FILE_APPEND);
    
        if($notificationType == "SUBSCRIBED") {
        }
        if ($notificationType == "EXPIRED" || $notificationType == "REFUND") {
        }
    } else {
        echo 'Header is not valid';
        exit;
    }
    

    what we just did:
    – decoded JWT
    – Saved the decoded response in the data.txt file
    so far so good, but we need to register our server-listener to the App-store connect so the Apple server will notify our server.

  2. Add your backend server url to Apps’ App Store Server notifications

 

Now we could navigate to the physical phone, run the app and make a purchase.
Make sure that once prompted for the password you add the test account password.

The payload will look like this:

{
  "bundleId" : "com.toninichev.Blue.InAppPurchaseTutorial",
  "currency" : "USD",
  "deviceVerification" : "fewrfewwg5y334wPOJMZrp40ih0WW\/rwlc2fRsYqixrsB9g",
  "deviceVerificationNonce" : "7dfggrtrq6-9bec-4811-af53-edsfdfe675d4",
  "environment" : "Sandbox",
  "expiresDate" : 1704816065000,
  "inAppOwnershipType" : "PURCHASED",
  "originalPurchaseDate" : 1704479596000,
  "originalTransactionId" : "2000000494141906",
  "price" : 990,
  "productId" : "InAppPurchaseRenewalPro",
  "purchaseDate" : 1704815765000,
  "quantity" : 1,
  "signedDate" : 1704815791564,
  "storefront" : "USA",
  "storefrontId" : "143441",
  "subscriptionGroupIdentifier" : "21429665",
  "transactionId" : "2000000496365380",
  "transactionReason" : "PURCHASE",
  "type" : "Auto-Renewable Subscription",
  "webOrderLineItemId" : "2000000047529194"
}

At the same time our backend server should have received server-2-server notification that purchase was made that will look like this:

[notificationType] => SUBSCRIBED
[subtype] => RESUBSCRIBE
[notificationUUID] => b19f4a0f-5e0b-4092-8286-0ceb29ed757f
[data] => stdClass Object
    (
        [appAppleId] => 6475326521
        [bundleId] => com.toninichev.Blue.InAppPurchaseTutorial
        [bundleVersion] => 1
        [environment] => Sandbox
        [signedTransactionInfo] => eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURD ...
        [signedRenewalInfo] => eyJhbGciOiJFUzI1NiIsIng....
        [status] => 1
    )

[version] => 2.0
[signedDate] => 1704815828988

and the decoded transaction info:

[transactionId] => 2000000496365380
[originalTransactionId] => 2000000494141906
[webOrderLineItemId] => 2000000047529194
[bundleId] => com.toninichev.Blue.InAppPurchaseTutorial
[productId] => InAppPurchaseRenewalPro
[subscriptionGroupIdentifier] => 21429665
[purchaseDate] => 1704815765000
[originalPurchaseDate] => 1704479596000
[expiresDate] => 1704816065000
[quantity] => 1
[type] => Auto-Renewable Subscription
[inAppOwnershipType] => PURCHASED
[signedDate] => 1704815827353
[environment] => Sandbox
[transactionReason] => PURCHASE
[storefront] => USA
[storefrontId] => 143441
[price] => 990
[currency] => USD

Create API keys generator

Now we have server-2-server notification set up, but if we want to use more of Apple’s APIs (like transaction history, purchases, etc) we have to create an app to generate signed JWT.
Apple won’t accept JWT that lives longer than 20 min. so we have to make sure that this is the maximup ttl that we set up.

const expirationTime = now + 900; // Set to 15 minutes (900 seconds)

There are two types of Apple APIs like it was stated above:

This app creates two type of JWT keys: one for the App Store Content APIs, and one for App Store ServerAPIs
Make sure that you have them right.

jwt-generator.js

import jwt from "jsonwebtoken";
import fs from "fs";

const now = Math.round(new Date().getTime() / 1000);
const expirationTime = now + 900; // Set to 15 minutes (900 seconds)

export default async (keyId, issuerId, privateKeyFileLocation, bundleId) => {

    const privateKey = fs.readFileSync(privateKeyFileLocation);

    // Create the JWT header and payload
    const header = {
      'alg': 'ES256',
      'kid': keyId,
      'typ': 'JWT'
    };

    const payload = {
      "iss": issuerId,
      "iat": now,
      "exp": expirationTime,
      "aud": 'appstoreconnect-v1',
    };

    if(bundleId)
        payload["bid"] = bundleId;

    console.log('payload: ', payload);

    // Generate the JWT
    const token = jwt.sign(payload, privateKey, { header: header, algorithm: 'ES256' });

    console.log(`Generated JWT: ${token}`);
    return token;
}

generate-token.js

import jwt from "./jwt-generator.js";
import keys from "./keys/keys.js";

function getAppStoreContentApiToken() {
    const keyId = keys.appStoreContentKeyId;
    const issuerId = keys.issuerId;
    const privateKeyFileLocation = keys.appStoreContentPrivateKeyFileLocation;
    jwt(keyId, issuerId, privateKeyFileLocation, null);
}



function getAppStoreServerApiToken() {
    const keyId = keys.inAppPurchaseKeyId;
    const issuerId = keys.issuerId;
    const privateKeyFileLocation = keys.inAppPurchasePrivateKeyFileLocation;
    const bundleId = keys.bundleId;

    jwt(keyId, issuerId, privateKeyFileLocation, bundleId);
}

export default {
    getAppStoreServerApiToken,
    getAppStoreContentApiToken
}

jwt-generator.js

import jwt from "jsonwebtoken";
import fs from "fs";

const now = Math.round(new Date().getTime() / 1000);
const expirationTime = now + 500; // Set to 15 minutes (900 seconds)

export default async (keyId, issuerId, privateKeyFileLocation, bundleId) => {

    const privateKey = fs.readFileSync(privateKeyFileLocation);

    // Create the JWT header and payload
    const header = {
      'alg': 'ES256',
      'kid': keyId,
      'typ': 'JWT'
    };

    const payload = {
      "iss": issuerId,
      "iat": now,
      "exp": expirationTime,
      "aud": 'appstoreconnect-v1',
    };

    if(bundleId)
        payload["bid"] = bundleId;

    console.log('payload: ', payload);

    // Generate the JWT
    const token = jwt.sign(payload, privateKey, { header: header, algorithm: 'ES256' });

    console.log(`Generated JWT: ${token}`);
    return token;
}

 

Now we could explore Apple’s APIs.

Example querying NotificationHistoryRequest

We could use PostMan

  1. Generate JWT using the app above.
  2. Add the url and make sure that the method is POST
  3. Add two headers:
    ‘Content-Type’ : ‘application/json’
    ‘Authorization’ : ‘Bearer XXXXXXXXX
    where XXXXXXXXX is the JWT generated from the app above.
  4. Navigate to Body and add start and end date. In example:
    {
      “startDate”:1703949010000,
      “endDate” :1704745240512,
    }


    Response will have signedPayload of type JWT. You could decode it here
    Response should look like this:

    {
      "notificationType": "SUBSCRIBED",
      "subtype": "INITIAL_BUY",
      "notificationUUID": "fdc70802-df14-4157-8d01-38ff04eaac0b",
      "data": {
        "appAppleId": 6475326521,
        "bundleId": "com.toninichev.Blue.InAppPurchaseTutorial",
        "bundleVersion": "1",
        "environment": "Sandbox",
        "signedTransactionInfo": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWZUbGZkMGZOdkZXdnpDMVlJQU5zWGpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJek1Ea3hNakU1TlRFMU0xb1hEVEkxTVRBeE1URTVOVEUxTWxvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCRUZFWWUvSnFUcXlRdi9kdFhrYXVESENTY1YxMjlGWVJWLzB4aUIyNG5DUWt6UWYzYXNISk9OUjVyMFJBMGFMdko0MzJoeTFTWk1vdXZ5ZnBtMjZqWFNqZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRkFNczhQanM2VmhXR1FsekUyWk9FK0dYNE9vL01BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpFQTh5Uk5kc2twNTA2REZkUExnaExMSndBdjVKOGhCR0xhSThERXhkY1BYK2FCS2pqTzhlVW85S3BmcGNOWVVZNVlBakFQWG1NWEVaTCtRMDJhZHJtbXNoTnh6M05uS20rb3VRd1U3dkJUbjBMdmxNN3ZwczJZc2xWVGFtUllMNGFTczVrPSIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJ0cmFuc2FjdGlvbklkIjoiMjAwMDAwMDQ5MjA0MjcwMiIsIm9yaWdpbmFsVHJhbnNhY3Rpb25JZCI6IjIwMDAwMDA0OTIwNDI3MDIiLCJ3ZWJPcmRlckxpbmVJdGVtSWQiOiIyMDAwMDAwMDQ3MDc1MTE0IiwiYnVuZGxlSWQiOiJjb20udG9uaW5pY2hldi5CbHVlLkluQXBwUHVyY2hhc2VUdXRvcmlhbCIsInByb2R1Y3RJZCI6IkluQXBwUHVyY2hhc2VSZW5ld2FsUHJvIiwic3Vic2NyaXB0aW9uR3JvdXBJZGVudGlmaWVyIjoiMjE0Mjk2NjUiLCJwdXJjaGFzZURhdGUiOjE3MDQyNTg2MTEwMDAsIm9yaWdpbmFsUHVyY2hhc2VEYXRlIjoxNzA0MjU4NjE4MDAwLCJleHBpcmVzRGF0ZSI6MTcwNDI1ODkxMTAwMCwicXVhbnRpdHkiOjEsInR5cGUiOiJBdXRvLVJlbmV3YWJsZSBTdWJzY3JpcHRpb24iLCJpbkFwcE93bmVyc2hpcFR5cGUiOiJQVVJDSEFTRUQiLCJzaWduZWREYXRlIjoxNzA0MjU4NjIzNzYxLCJlbnZpcm9ubWVudCI6IlNhbmRib3giLCJ0cmFuc2FjdGlvblJlYXNvbiI6IlBVUkNIQVNFIiwic3RvcmVmcm9udCI6IlVTQSIsInN0b3JlZnJvbnRJZCI6IjE0MzQ0MSIsInByaWNlIjo5OTAsImN1cnJlbmN5IjoiVVNEIn0.1TnBWCm6WkmarcFsMVA_tYjmtLe2F6qKZVAUy_Y2j6Ki2vLA9KGW8xih3PZUb1UizXFbN-BbV7gnYxmPC4QlEg",
        "signedRenewalInfo": "eyJhbGciOiJFUzI1NiIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWZUbGZkMGZOdkZXdnpDMVlJQU5zWGpBS0JnZ3Foa2pPUFFRREF6QjFNVVF3UWdZRFZRUURERHRCY0hCc1pTQlhiM0pzWkhkcFpHVWdSR1YyWld4dmNHVnlJRkpsYkdGMGFXOXVjeUJEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURUxNQWtHQTFVRUN3d0NSell4RXpBUkJnTlZCQW9NQ2tGd2NHeGxJRWx1WXk0eEN6QUpCZ05WQkFZVEFsVlRNQjRYRFRJek1Ea3hNakU1TlRFMU0xb1hEVEkxTVRBeE1URTVOVEUxTWxvd2daSXhRREErQmdOVkJBTU1OMUJ5YjJRZ1JVTkRJRTFoWXlCQmNIQWdVM1J2Y21VZ1lXNWtJR2xVZFc1bGN5QlRkRzl5WlNCU1pXTmxhWEIwSUZOcFoyNXBibWN4TERBcUJnTlZCQXNNSTBGd2NHeGxJRmR2Y214a2QybGtaU0JFWlhabGJHOXdaWElnVW1Wc1lYUnBiMjV6TVJNd0VRWURWUVFLREFwQmNIQnNaU0JKYm1NdU1Rc3dDUVlEVlFRR0V3SlZVekJaTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEEwSUFCRUZFWWUvSnFUcXlRdi9kdFhrYXVESENTY1YxMjlGWVJWLzB4aUIyNG5DUWt6UWYzYXNISk9OUjVyMFJBMGFMdko0MzJoeTFTWk1vdXZ5ZnBtMjZqWFNqZ2dJSU1JSUNCREFNQmdOVkhSTUJBZjhFQWpBQU1COEdBMVVkSXdRWU1CYUFGRDh2bENOUjAxREptaWc5N2JCODVjK2xrR0taTUhBR0NDc0dBUVVGQndFQkJHUXdZakF0QmdnckJnRUZCUWN3QW9ZaGFIUjBjRG92TDJObGNuUnpMbUZ3Y0d4bExtTnZiUzkzZDJSeVp6WXVaR1Z5TURFR0NDc0dBUVVGQnpBQmhpVm9kSFJ3T2k4dmIyTnpjQzVoY0hCc1pTNWpiMjB2YjJOemNEQXpMWGQzWkhKbk5qQXlNSUlCSGdZRFZSMGdCSUlCRlRDQ0FSRXdnZ0VOQmdvcWhraUc5Mk5rQlFZQk1JSCtNSUhEQmdnckJnRUZCUWNDQWpDQnRneUJzMUpsYkdsaGJtTmxJRzl1SUhSb2FYTWdZMlZ5ZEdsbWFXTmhkR1VnWW5rZ1lXNTVJSEJoY25SNUlHRnpjM1Z0WlhNZ1lXTmpaWEIwWVc1alpTQnZaaUIwYUdVZ2RHaGxiaUJoY0hCc2FXTmhZbXhsSUhOMFlXNWtZWEprSUhSbGNtMXpJR0Z1WkNCamIyNWthWFJwYjI1eklHOW1JSFZ6WlN3Z1kyVnlkR2xtYVdOaGRHVWdjRzlzYVdONUlHRnVaQ0JqWlhKMGFXWnBZMkYwYVc5dUlIQnlZV04wYVdObElITjBZWFJsYldWdWRITXVNRFlHQ0NzR0FRVUZCd0lCRmlwb2RIUndPaTh2ZDNkM0xtRndjR3hsTG1OdmJTOWpaWEowYVdacFkyRjBaV0YxZEdodmNtbDBlUzh3SFFZRFZSME9CQllFRkFNczhQanM2VmhXR1FsekUyWk9FK0dYNE9vL01BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0Jnc0JCQUlGQURBS0JnZ3Foa2pPUFFRREF3Tm9BREJsQWpFQTh5Uk5kc2twNTA2REZkUExnaExMSndBdjVKOGhCR0xhSThERXhkY1BYK2FCS2pqTzhlVW85S3BmcGNOWVVZNVlBakFQWG1NWEVaTCtRMDJhZHJtbXNoTnh6M05uS20rb3VRd1U3dkJUbjBMdmxNN3ZwczJZc2xWVGFtUllMNGFTczVrPSIsIk1JSURGakNDQXB5Z0F3SUJBZ0lVSXNHaFJ3cDBjMm52VTRZU3ljYWZQVGp6Yk5jd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NakV3TXpFM01qQXpOekV3V2hjTk16WXdNekU1TURBd01EQXdXakIxTVVRd1FnWURWUVFERER0QmNIQnNaU0JYYjNKc1pIZHBaR1VnUkdWMlpXeHZjR1Z5SUZKbGJHRjBhVzl1Y3lCRFpYSjBhV1pwWTJGMGFXOXVJRUYxZEdodmNtbDBlVEVMTUFrR0ExVUVDd3dDUnpZeEV6QVJCZ05WQkFvTUNrRndjR3hsSUVsdVl5NHhDekFKQmdOVkJBWVRBbFZUTUhZd0VBWUhLb1pJemowQ0FRWUZLNEVFQUNJRFlnQUVic1FLQzk0UHJsV21aWG5YZ3R4emRWSkw4VDBTR1luZ0RSR3BuZ24zTjZQVDhKTUViN0ZEaTRiQm1QaENuWjMvc3E2UEYvY0djS1hXc0w1dk90ZVJoeUo0NXgzQVNQN2NPQithYW85MGZjcHhTdi9FWkZibmlBYk5nWkdoSWhwSW80SDZNSUgzTUJJR0ExVWRFd0VCL3dRSU1BWUJBZjhDQVFBd0h3WURWUjBqQkJnd0ZvQVV1N0Rlb1ZnemlKcWtpcG5ldnIzcnI5ckxKS3N3UmdZSUt3WUJCUVVIQVFFRU9qQTRNRFlHQ0NzR0FRVUZCekFCaGlwb2RIUndPaTh2YjJOemNDNWhjSEJzWlM1amIyMHZiMk56Y0RBekxXRndjR3hsY205dmRHTmhaek13TndZRFZSMGZCREF3TGpBc29DcWdLSVltYUhSMGNEb3ZMMk55YkM1aGNIQnNaUzVqYjIwdllYQndiR1Z5YjI5MFkyRm5NeTVqY213d0hRWURWUjBPQkJZRUZEOHZsQ05SMDFESm1pZzk3YkI4NWMrbGtHS1pNQTRHQTFVZER3RUIvd1FFQXdJQkJqQVFCZ29xaGtpRzkyTmtCZ0lCQkFJRkFEQUtCZ2dxaGtqT1BRUURBd05vQURCbEFqQkFYaFNxNUl5S29nTUNQdHc0OTBCYUI2NzdDYUVHSlh1ZlFCL0VxWkdkNkNTamlDdE9udU1UYlhWWG14eGN4ZmtDTVFEVFNQeGFyWlh2TnJreFUzVGtVTUkzM3l6dkZWVlJUNHd4V0pDOTk0T3NkY1o0K1JHTnNZRHlSNWdtZHIwbkRHZz0iLCJNSUlDUXpDQ0FjbWdBd0lCQWdJSUxjWDhpTkxGUzVVd0NnWUlLb1pJemowRUF3TXdaekViTUJrR0ExVUVBd3dTUVhCd2JHVWdVbTl2ZENCRFFTQXRJRWN6TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd0hoY05NVFF3TkRNd01UZ3hPVEEyV2hjTk16a3dORE13TVRneE9UQTJXakJuTVJzd0dRWURWUVFEREJKQmNIQnNaU0JTYjI5MElFTkJJQzBnUnpNeEpqQWtCZ05WQkFzTUhVRndjR3hsSUVObGNuUnBabWxqWVhScGIyNGdRWFYwYUc5eWFYUjVNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVFzd0NRWURWUVFHRXdKVlV6QjJNQkFHQnlxR1NNNDlBZ0VHQlN1QkJBQWlBMklBQkpqcEx6MUFjcVR0a3lKeWdSTWMzUkNWOGNXalRuSGNGQmJaRHVXbUJTcDNaSHRmVGpqVHV4eEV0WC8xSDdZeVlsM0o2WVJiVHpCUEVWb0EvVmhZREtYMUR5eE5CMGNUZGRxWGw1ZHZNVnp0SzUxN0lEdll1VlRaWHBta09sRUtNYU5DTUVBd0hRWURWUjBPQkJZRUZMdXczcUZZTTRpYXBJcVozcjY5NjYvYXl5U3JNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEZ1lEVlIwUEFRSC9CQVFEQWdFR01Bb0dDQ3FHU000OUJBTURBMmdBTUdVQ01RQ0Q2Y0hFRmw0YVhUUVkyZTN2OUd3T0FFWkx1Tit5UmhIRkQvM21lb3locG12T3dnUFVuUFdUeG5TNGF0K3FJeFVDTUcxbWloREsxQTNVVDgyTlF6NjBpbU9sTTI3amJkb1h0MlFmeUZNbStZaGlkRGtMRjF2TFVhZ002QmdENTZLeUtBPT0iXX0.eyJvcmlnaW5hbFRyYW5zYWN0aW9uSWQiOiIyMDAwMDAwNDkyMDQyNzAyIiwiYXV0b1JlbmV3UHJvZHVjdElkIjoiSW5BcHBQdXJjaGFzZVJlbmV3YWxQcm8iLCJwcm9kdWN0SWQiOiJJbkFwcFB1cmNoYXNlUmVuZXdhbFBybyIsImF1dG9SZW5ld1N0YXR1cyI6MSwic2lnbmVkRGF0ZSI6MTcwNDI1ODYyMzc2MSwiZW52aXJvbm1lbnQiOiJTYW5kYm94IiwicmVjZW50U3Vic2NyaXB0aW9uU3RhcnREYXRlIjoxNzA0MjU4NjExMDAwLCJyZW5ld2FsRGF0ZSI6MTcwNDI1ODkxMTAwMH0.TfMBSIU2kTu97Zs_V-MoOSaDPNdcKsaBKVodr46mZsgSvIY5LrTTSlPBgNj6BFlys2lYyMYAA2PrzJu-O5zxtA",
        "status": 1
      },
      "version": "2.0",
      "signedDate": 1704258625515
    }

     

    You could use jwt.io to decode signedTransactionInfo and signedRenewalInfo the same way.

    signedTransactionInfo

    {
      "transactionId": "2000000492042702",
      "originalTransactionId": "2000000492042702",
      "webOrderLineItemId": "2000000047075114",
      "bundleId": "com.toninichev.Blue.InAppPurchaseTutorial",
      "productId": "InAppPurchaseRenewalPro",
      "subscriptionGroupIdentifier": "21429665",
      "purchaseDate": 1704258611000,
      "originalPurchaseDate": 1704258618000,
      "expiresDate": 1704258911000,
      "quantity": 1,
      "type": "Auto-Renewable Subscription",
      "inAppOwnershipType": "PURCHASED",
      "signedDate": 1704258623761,
      "environment": "Sandbox",
      "transactionReason": "PURCHASE",
      "storefront": "USA",
      "storefrontId": "143441",
      "price": 990,
      "currency": "USD"
    }

     

    signedRenewalInfo

    {
      "originalTransactionId": "2000000492042702",
      "autoRenewProductId": "InAppPurchaseRenewalPro",
      "productId": "InAppPurchaseRenewalPro",
      "autoRenewStatus": 1,
      "signedDate": 1704258623761,
      "environment": "Sandbox",
      "recentSubscriptionStartDate": 1704258611000,
      "renewalDate": 1704258911000
    }

     

Using Oauth2 to sign-in users with Apple using Java Script Library

Example project here

<html>
    <head>
      <style>
        h1 {
          text-align: center;
        }

        #welcomePanel {
          display: none;
          text-align: center;
        }

        #signInPanel h2 {
          text-align: center;
          border: 1px solid silver;
        }

        #google-auth-button {
          -webkit-box-align: baseline;
          align-items: baseline;
          border-width: 0px;
          border-radius: 3px;
          box-sizing: border-box;
          display: inline-flex;
          font-size: inherit;
          font-style: normal;
          font-family: inherit;
          max-width: 100%;
          position: relative;
          text-align: center;
          text-decoration: none;
          transition: background 0.1s ease-out 0s, box-shadow 0.15s cubic-bezier(0.47, 0.03, 0.49, 1.38) 0s;
          white-space: nowrap;
          cursor: pointer;
          padding: 0px 10px;
          vertical-align: middle;
          width: 100%;
          -webkit-box-pack: center;
          justify-content: center;
          font-weight: bold;
          color: var(--ds-text,#42526E) !important;
          height: 40px !important;
          line-height: 40px !important;
          background: rgb(255, 255, 255) !important;
          box-shadow: rgb(0 0 0 / 20%) 1px 1px 5px 0px !important;
        }
      </style>
      <!-- First we have to include Apple Sign-in script -->
      <script
        type="text/javascript"
        src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
    </head>
    <body>
      <script>

        function parseJwt (token) {
            var base64Url = token.split('.')[1];
            var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
            var jsonPayload = decodeURIComponent(window.atob(base64).split('').map(function(c) {
                return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
            }).join(''));

            return JSON.parse(jsonPayload);
        }

        /**
         * This function will initialize the `AppleID.auth` object with parameter we pass in.
         */
       
        const initApple = () => {
          window.AppleID.auth.init({
            clientId: "com.toni-develops.sign-in-with-apple-identifier", // This is the service ID we created.
            scope: "name email", // To tell apple we want the user name and emails fields in the response it sends us.
            redirectURI: "https://www.toni-develops.com/external-files/examples/oauth-with-apple-using-js-library", // As registered along with our service ID
            state: "origin:web", // Any string of your choice that you may use for some logic. It's optional and you may omit it.
            usePopup: true, // Important if we want to capture the data apple sends on the client side.
          });
        };        
        
        
        /**
         * This function is where the magic happens.
         * This is a simple example, ideally you'll have catch block as well to handle authentication failure.
         */
        const singInApple = async () => {
          const response = await window.AppleID.auth.signIn();

          return response;
        };     
        
        initApple();


        function signInWithApple() {
          const userData = singInApple();

          userData.then( (data) => {
            console.dir(data, { depth: null });
            const result = parseJwt(data.authorization.id_token)
            console.dir(result, { depth: null });

            document.querySelector('#signInPanel').innerHTML = '<h2>Welcome ' + result.email + '</h2>';
          });
        }
      </script>
      <h1>Sign-In with Apple using Java Script library example</h1>
      <div id="signInPanel">
        <button id="google-auth-button" class="css-11s2kpt" type="button" tabindex="0" onclick="signInWithApple()">
          <span class="css-1ujqpe8">
            <img class="appleLogo" src="https://www.toni-develops.com/external-files/examples/assets/apple-logo.svg" alt="">
          </span>
          <span class="css-19r5em7"><span>Continue with Apple</span>
        </button> 
      </div>
    </body>
</html>

 

Longest Common Substring

[tabby title=”Task”]

Given two strings text1 and text2, return the length of their longest common subsequenceIf there is no common subsequence, return 0.

subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.

  • For example, "ace" is a subsequence of "abcde".

common subsequence of two strings is a subsequence that is common to both strings.

 

Example 1:

Input:

 text1 = "abcde", text2 = "ace" 

Output:

 3  

Explanation:

 The longest common subsequence is "ace" and its length is 3.

Example 2:

Input:

 text1 = "abc", text2 = "abc"

Output:

 3

Explanation:

 The longest common subsequence is "abc" and its length is 3.

Example 3:

Input:

 text1 = "abc", text2 = "def"

Output:

 0

Explanation:

 There is no such common subsequence, so the result is 0.

 

Constraints:

  • 1 <= text1.length, text2.length <= 1000
  • text1 and text2 consist of only lowercase English characters.

This problem was taken from Leetcode Longest Common Subsequence

 

[tabby title=”Solution”]

Dynamic programming with memoization.

We create a matrix to store (memoize) the response from previous calculations so we won’t re-calculate them again.

We are starting from the first row comparing every character from column 1 with the first character from row one.

There are two cases:

  • either the character match, then we add 1 and add it with the value from the diagonal to the left of the cell where we are.

Longest Common Substring Step 1

  • the characters don’t match, then we get the MAX of the value above current cell  and the value to the left of the current cell.

Longest Common Substring Step 2

Here again c int the column (first string) matches c in the row (the second string)

so we get the value of last comparison (to the left and up diagonal of the current cell)  which is 1 and add 1 again since characters match.

Longest Common Substring 3

and keep going till we reach the end of both strings which is the answer.

 

Longest Common Substring 4

 

/**
 * @param {string} text1
 * @param {string} text2
 * @return {number}
 */
var longestCommonSubsequence = function(text1, text2) {
    var txt1 = text1.split('');
    var txt2 = text2.split('');

    txt1.unshift('0');
    txt2.unshift('1');

    var l1 = txt1.length;
    var l2 = txt2.length;

    var matrix = [];
    for(var i = 0; i < l2; i ++) {
        matrix[i] = new Array(l1).fill(0);
    }

    var maxCommon = 0;
    
    for(var row = 0; row < l2; row ++) {
        for(var col = 0; col < l1; col ++) {
            var last = 0;

            if(txt1[col] == txt2[row]) {
                var previousDiagonalRowVal = row == 0 || col == 0  ? 0 : matrix[row - 1][col - 1];
                last =  1 + previousDiagonalRowVal;
            }
            else {
                var prevUp = row == 0 ?  0 : matrix[row - 1][col];
                var prevLeft = col == 0 ? 0 : matrix[row][col - 1];               
                last = Math.max(prevUp, prevLeft);
            }
            matrix[row][col] = last;
            maxCommon = last > maxCommon ? last : maxCommon;
    
        }
    }    
    return maxCommon;
};

var text1 = "abcde", text2 = "ace";
var r = longestCommonSubsequence(text1, text2);
console.log(">>", r);

[tabbyending]

Trapping Rain Water

[tabby title=”Task”]

Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it is able to trap after raining.


image was borrowed from leetcode

The above elevation map is represented by array [0,1,0,2,1,0,1,3,2,1,2,1]. In this case, 6 units of rain water (blue section) are being trapped. Thanks Marcos for contributing this image!

Example:

Input:

 [0,1,0,2,1,0,1,3,2,1,2,1]

Output:

 6

This problem was taken from Leetcode

[tabby title=”Solution”]

 

The brute force approach: for each element we go to the right and find the maximum height of the bar, then we go to the left and do the same.

For any element the maximum amount of the water that could be trapped will be the minimum of left height and right height, minus the height of the bar.

So for the array [0,1,0,2,1,0,1,3,2,1,2,1] we go all the way to the right and calculate the max right value, starting from first element ‘0’ max right will be 0. ‘1’ – max right is ‘1’ and so on.
We repeat the same from last element ‘1’ to the first one.

Then the trapped water for the first column will be:  min(maxRight, maxLeft) – theArrayElement[n]

the array 0 1 0 2 1 0 1 3 2 1 2 1
max right 0 1 1 2 2 2 2 3 3 3 3 3
max left 3 3 3 3 3 3 3 3 2 2 2 1
collected
water
0 0 1 0 1 2 1 0 0 1 0 0

 

The complexity will be O(n2)

/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function(height) {
    if(height.length < 2)
        return 0;

    let findMaxLeft = function(idx, height) {
        let max = 0;
        for(let i =idx;i >= 0; i --) {
            max = Math.max(max, height[i]);
        }
        return max;
    }

    let findMaxRight = function(idx, height) {
        let max = 0;
        for(let i = idx;i < height.length; i ++) {
            max = Math.max(max, height[i]);
        }
        return max;
    }  

    let collectedWater = 0;
    for(let i = 0;i < height.length; i ++) {

        const maxLeft = findMaxLeft(i, height);
        const maxRight = findMaxRight(i, height);

        let min = Math.min(maxLeft, maxRight);
        collectedWater += (min - height[i]);
    }

    return collectedWater;
};

The better solution: find all max left and max right with one loop, then do a second loop for each element in the array, and calculate trapped water.

/**
 * @param {number[]} height
 * @return {number}
 */
var trap = function(height) {
    let maxLeftArray = [], maxRightArray = [];
    let maxLeft = 0, maxRight = 0;
    const ln = height.length;
    let trappedWater = 0;

    for(let i = 0;i < height.length; i ++) {
        maxLeftArray[i] = Math.max(height[i], maxLeft);
        maxLeft = maxLeftArray[i];

        maxRightArray[ln - i - 1] = Math.max(height[ln - i - 1], maxRight);
        maxRight = maxRightArray[ln - i - 1];
    }

    for(let i = 0;i < height.length; i ++) {
        trappedWater += Math.min(maxLeftArray[i], maxRightArray[i]) - height[i];
    }
    return trappedWater;

};
what we just did:

– With one loop find the max left and right bar on each side.
– for any element the maximum amount of the water that could be trapped will be the minimum of left height and right height, minus the height of the bar.

[tabbyending]

Array VS Hash Table

Hash table tutorial

Find element in Array

function fuindInArray() {
  var t0 = performance.now();

  for(var q = 0; q < data2.length; q++) {
      if( data2[q] == '106112407') {
          console.log(data2["106112407"]);
          break;
      }
  }

Find element in Hash Table

function findInHashtable() {
  var t0 = performance.now();

  console.log(data1["106112407"]);

  var t1 = performance.now();
  document.querySelector('#result1').value = "Call took " + (t1 - t0) + " milliseconds.";
}

 


Sort an array

[tabby title=”Task”]

Given an array of integers nums, sort the array in ascending order.

Example 1:

Input:

 [5,2,3,1]

Output:

 [1,2,3,5]

Example 2:

Input:

[5,1,1,2,0,0]

Output:

[0,0,1,1,2,5]

 

This problem was taken from Leetcode

[tabby title=”Solution”]

The brute force solution.

The brute force solution could be to make two loops and iterate through each elements in the array and repeatedly swapping the adjacent elements if they are in wrong order. This approach is called Bubble sort.

var nums = [5,4,6,1,2,3];


/**
 * @param {number[]} nums
 * @return {number[]}
 */
var sortArray = function(nums) {

  for(var i = 0; i < nums.length - 1;i++) {
    for(var j=0;j < nums.length - 1; j++) {
      if(nums[j] > nums[j+1]) {
        var tmp = nums[j];
        nums[j] = nums[j + 1];
        nums[j + 1] = tmp;
      }
    }
  }
  return nums;
}

nums = sortArray(nums);
console.log(nums);

In the worst case scenario the complexity will be O(n*n) since we have to iterate n*n times where n is the number of elements in the array.

A better performing solution is to keep splitting the array into two halves till we have single elements. Then we start merging the arrays and sorting at the same time. This is called merge sort algorithm.

The diagram is borrowed from Wikipedia.

 

Merge sort algorithm implementation:

/**
 * @param {number[]} nums
 * @return {number[]}
 */
var sortArray = function(nums) {
  function mergeArray(nums, start, mid, end) {
    var i = start, j = mid + 1;
    var tempArr = [];
    // compare till we reach either end of one of the halves
    for(var k = start;(i < mid+1 && j < end+1); k++) {
        if(nums[i] <= nums[j]) {
          tempArr.push(nums[i]);
          i++;           
        }
        else {
          tempArr.push(nums[j]);
          j ++;            
        }       
    }    
    
    // add the rest from the first half 
    for(var k = j;k < end + 1; k++) {
      tempArr.push(nums[k]);
    }         
    // add the rest from the second half 
    for(var k = i;k < mid + 1; k++) {
      tempArr.push(nums[k]);
    }         

    // set up nums with sorted values
    for(var k = start;k < end+1; k++) {
      nums[k] = tempArr[k - start];      
    }
  }

  function mergeSort(nums, start, end) {
    var mid = Math.floor((start + end) / 2);
    if(start < end) {
        mergeSort(nums, start, mid);
        mergeSort(nums, mid + 1, end);
        mergeArray(nums, start, mid, end);
    }
  }
  mergeSort(nums, 0, nums.length - 1);
  return nums;
}
var nums = [5,4,6,1,2,3];
var result = sortArray(nums);
console.log(result);

What we just did?
– started to split the array by half (line 39,40) which recursively calls mergeSort and keep splitting on smaller and smaller pieces till the array has only 1 element (line 37)

mergeSortcall sequence:

Caller Start End
Initial Call 0 5
L 0 2
L 0 1
L 0 0
R 1 1
R 2 2
R 3 5
L 3 4
L 3 3
R 4 4
R 5 5

[tabbyending]

Adding multiple brands and applications

branch-name:  
Click To Copy

 

Let’s make the task more challenging and assume that we are going to have two different Brands (or apps)

one.localhost.com,
two.localhost.com,

for the most of the cases they will do the same thing but the styling will be different, so let’s add ability for our components to be styled in different ways, depending of the brand.

Passing the sub-domain to the PageLayout component

Let’s add new parameter to store the default brand.

./.env

APP_NAME=Webpack React Tutorial
GRAPHQL_URL=http://localhost:4001/graphql
PROD_SERVER_PORT=3006
DEFAULT_BRAND=one

and make it available to the front end.

./getEnvironmentConstants.js

...
const frontendConstants = [
  'APP_NAME',
  'GRAPHQL_URL',
  'PROD_SERVER_PORT',
  'DEFAULT_BRAND'
];
...

Now, let’s pass the sub domain to the PageComponent. We have to do this in two different places: one for the client side, and one on the SSR.

./components/App/index.js

import React from 'react';
import PageLayout from '../../containers/PageLayout';
import { ApolloProvider } from 'react-apollo';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore} from 'redux';
import reducers from '../../reducers';

const styles = require('./styles.scss');

const store = createStore(reducers, {});

export default () => {
  const GRAPHQL_URL = process.env.GRAPHQL_URL;
  const client = new ApolloClient({
    link: new HttpLink({ uri:  GRAPHQL_URL }),
    cache: new InMemoryCache().restore(window.__APOLLO_STATE__),
  }); 
  
  const subDomain = window.location.host.split('.').length == 1 ? process.env.DEFAULT_BRAND : window.location.host.split('.')[0];
  return (
    <div className={styles.appWrapper}>
      <Provider store={store}>
        <ApolloProvider client={client}>
          <Router>
            <Switch>
            <Route exact path="*" render={(props) => <PageLayout {...props} subDomain={subDomain} />} />
            </Switch>
          </Router>
        </ApolloProvider>
      </Provider>
    </div>        
  );
}

 what we just did:
– (line 23) getting the sub domain
– (line 30) passing extra parameter with the subDomain to the PageComponent

Do the same for the SSR

./components/App/ssr-index.js

import React from 'react';
import PageLayout from '../../containers/PageLayout';
import { StaticRouter,  Route, Switch } from 'react-router-dom';
import { ApolloProvider } from 'react-apollo';
import { Provider } from 'react-redux';
import { createStore} from 'redux';
import reducers from '../../reducers';
import fetch from 'isomorphic-fetch';

import styles from './styles.scss';

const store = createStore(reducers, {});

export default ( {req, client} ) => {
  const subDomain = req.headers.host.split('.').length == 1 ? process.env.DEFAULT_BRAND : req.headers.host.split('.')[0];
  const context = {};
  return (
    <div className={styles.appWrapper}>
      <Provider store={store}>   
        <ApolloProvider client={client}>            
          <StaticRouter location={ req.url } context={context}>
            <Switch>
              <Route exact path="*" render={(props) => <PageLayout {...props} subDomain={subDomain} />} />  
            </Switch>            
          </StaticRouter>
        </ApolloProvider>
      </Provider>
    </div>
  );
}

 what we just did:
– (line 23) getting the sub domain, this time from the express request object, since this is happening on the server side.
– (line 30) passing extra parameter with the subDomain to the PageComponent

 

Now this will work fine for the production build, but if you run yarn start-api it will break with  Invalid Host header message.  This is a security feature and we need to tell Webpack dev server that we are going to have sub-domains.
This could be done by passing disableHostCheck: true and since we are using WebpackDevServer only for development this should be safe enough.

./server-api.js

import WebpackDevServer from 'webpack-dev-server';
import webpack from 'webpack';
import config from './webpack.api.config.js';
require('dotenv').config();

console.log(">>>" + process.env.GRAPHQL_URL);

const compiler = webpack(config);
const server = new WebpackDevServer(compiler, {
  hot: true,
  publicPath: config.output.publicPath,
  historyApiFallback: true,
  disableHostCheck: true,
});
server.listen(8080, 'localhost', function() {});

For the CLI config, if will look like this: --disable-host-check

./package.json

...
    "start-cli": "webpack-dev-server --disable-host-check --hot --history-api-fallback --config webpack.cli.config.js",
...

 

 

Passing brand name to the components

This could be achieved in many different ways:
– we could either use redux and add the brand property there, exposing it to all connected components (which we will do later in this tutorial)
– or we could simply pass it as a property from the PageLayout component.

Let’s do the second one since it is straight forward.

./containers/PageLayout/index.js

import React, { Component } from 'react';
import ComponentList from './ComponentList';
import Loading from '../../components/Loading';
import query from './query';
import { graphql } from 'react-apollo';
const styles = require('./styles.scss');
class PageLayout extends Component {
    constructor(props) {
      super(props);      
    } 
  
    render() {
      if(!this.props.data.getPageByUrl) {
        return (<Loading />);
      }     

      const subDomain = this.props.subDomain;

      const allLayout = this.props.data.getPageByUrl.layout.map((layoutList) => {
        const layout = layoutList.components.map((component, id , components) => {
          const componentName = component.name;        
          const ChildComponent = ComponentList[componentName];
          if(typeof ChildComponent === 'undefined') {
            return(
              <div key='{id}' className={styles.error}>Can't find {componentName} component!</div>
            );
          }
          return (
              <ChildComponent key={componentName} subDomain={subDomain}  />
          );
        });
        return layout;
      });
      return(
        <div className={styles.app}>
          {allLayout}
        </div>
      );
    }
}
export default graphql(query, {
    options(props) {
      return {
        variables: {
          url: props.history.location.pathname
        },
      };
    },
  })(PageLayout);

 

Adding brand specific styling to the component

Coming up with a good naming convention and folder structure is very important, but I will leave this to you to research.

I would propose the simplest one:

component
|- brands
|  |_ one
|     |- styles.scss
|  |_ two
|     |- styles.scss
|- index.js

./components/Home/index.js

import React from 'react';

const Home = ( {subDomain} ) => {
  const styles = require(`./brands/${subDomain}/styles.scss`);

  return (
    <div>
      <div className={styles.wrapper}>This is my home section!</div>
    </div>
  )
}
export default Home;

yarn start

hit one.localhost:3006/home and then two.localhost:3006/home and you will notice the difference right away!

But how this was achieved without re-rendering the component?

Let’s look at component’s css file (it might be 1.css or different number but look at the net tab and make sure that the CSS contains something like this):

.one-wrapper{background:#8d8dac;color:#fff}.one-wrapper,.two-wrapper{text-align:center;font-family:MyFont}.two-wrapper{background:#fff;color:#000}

– first brand has .one-wrapper{background:#8d8dac;color:#fff} and the second has two-wrapper{background:#fff;color:#000} and all CSS that is in common is added like this: .one-wrapper,.two-wrapper{text-align:center;font-family:MyFont} so no repeating CSS if it’s the same between brands.
Basically now the css file for this component will have all styling for both brands.
This is nice but not perfect. We still load unnecessarily the styles for the brands that we are not in to. Let’s see how to fix this in the next chapter.

branch-name:  
Click To Copy

 

 

 

Adding html.js component

branch-name:  
Click To Copy

 

Let’s clean up the code a bit and move the html code from ./ssr-server.js to a separate component called html.js.

./html.js

import React from 'react';

const Html = ({ content, cssBundles, jsBundles, apolloClient }) => (
  <html lang="en">
  <head>
    <meta charSet="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Server Side Rendering and Bundle Splitting</title>
    <link
    href="/dist/main.css"
    rel="stylesheet"
    as="style"
    media="screen, projection"
    type="text/css"
    charSet="UTF-8"
  />

    {
      cssBundles.map( (bundle) => 
        (<link
          href={`${bundle.publicPath}`}
          rel="stylesheet"
          as="style"
          media="screen, projection"
          type="text/css"
          charSet="UTF-8"
        />))
    }

    {jsBundles.map( ( {file}) => (<script src={`/dist/${file}`}>{file}</script>) )}
  </head>
  <body cz-shortcut-listen="true">
    <div id="root" dangerouslySetInnerHTML={{ __html: content }} />
    <script dangerouslySetInnerHTML={{
          __html: `window.__APOLLO_STATE__=${JSON.stringify(apolloClient.cache.extract())}`}} />
  
    <script src="/dist/main-bundle.js"></script>
  </body>
</html>  

);

export default Html;

what we just did:
– we created a plain react component that will accept 3 parameters:
    – content [string]: rendered components returned from react-apollo  renderToStringWithData(…)
    – cssBundles [array of CSS bundle objects]:
    – jsBundles [array of js bundle objects]:
apolloClient [Object]:
An instance of the Apollo client.

 

And now, let’s remove the HTML coder from ./server.js and add the new component instead, passing all necessary parameters.

./server-js

import React from 'react';
import express from 'express';
import App from './src/components/App/ssr-index';
import Loadable from 'react-loadable';
import manifest from './dist/loadable-manifest.json';
import { getDataFromTree } from "react-apollo";
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { renderToStringWithData } from "react-apollo"
import { createHttpLink } from 'apollo-link-http';
import { getBundles } from 'react-loadable/webpack';
import ReactDOMServer from 'react-dom/server';
import Html from './html.js';

const PORT = process.env.PROD_SERVER_PORT;
const app = express();

app.use('/server-build', express.static('./server-build'));
app.use('/dist', express.static('dist')); // to serve frontent prod static files
app.use('/favicon.ico', express.static('./src/images/favicon.ico'));

app.get('/*', (req, res) => {

  const GRAPHQL_URL = process.env.GRAPHQL_URL;
  const client = new ApolloClient({
    ssrMode: true,
    link: createHttpLink({
     uri: GRAPHQL_URL,
     fetch: fetch,
     credentials: 'same-origin',
     headers: {
       cookie: req.header('Cookie'),
     },
   }), 
    cache: new InMemoryCache()
  });    

  // Prepare to get list of all modules that have to be loaded for this route
  const modules = [];
  const mainApp = (
    <Loadable.Capture report={moduleName => modules.push(moduleName)}>
      <App req={req} client={client} />
    </Loadable.Capture>    
  );

  // Execute all queries and fetch the results before continue
  getDataFromTree(mainApp).then(() => {        
    // Once we have the data back, this will render the components with the appropriate GraphQL data.
    renderToStringWithData(<App req={req} client={client} />).then( (HTML_content) => {
      // Extract CSS and JS bundles
      const bundles = getBundles(manifest, modules); 
      const cssBundles = bundles.filter(bundle => bundle && bundle.file.split('.').pop() === 'css');
      const jsBundles = bundles.filter(bundle => bundle && bundle.file.split('.').pop() === 'js');
    
      const html = <Html content={HTML_content} cssBundles={cssBundles} jsBundles={jsBundles} apolloClient={client} />;

      res.status(200);
      res.send(`<!doctype html>\n${ReactDOMServer.renderToStaticMarkup(html)}`);
      res.end(); 
    });    



  }).catch( (error) => {
    console.log("ERROR !!!!", error);
  });
});

Loadable.preloadAll().then(() => {
  app.listen(PORT, () => {
    console.log(`? Server is listening on port ${PORT}`);
  });
});
branch-name:  
Click To Copy

 

 

Sorting algorithms

[tabby title=”Task”]

Sort an array of integers in descending order.

Example:

Given an array of 4 elements

 [3,15,1,5]

Produce an array that will look like this:

 [1,3,5,15]

 

[tabby title=”Solution”]

Brute force using the “selection sort” algorithm.

  1. Starting with the first number 3 we assume that this is our smallest number since we don’t know the others.
  2. Then we iterate through the whole array comparing 3 with other numbers.
  3. If we find number that is smaller than 3 (in our case 1) this will become our smallest number, and we continue comparing it to the end of the array.
  4. when we reach the end of the array we swap the first number, that we assumed that is the smallest one with the newly discovered smallest number (in our case number 1 at third position), and the array will look like this now: [1,15,3,5]
  5. Now as we know for sure that the number at the first position (number 1) is the smallest one, we can start from position 2 and repeat steps 2 to 5 till we checked all numbers in the array.

 

function sortArray(list) {
    var smallestIndex;    
    for(var i = 0; i < list.length; i ++) {
        smallestIndex = i;
        for(var ii = i + 1; ii < list.length;ii ++) {
            smallestIndex = list[ii] < list[smallestIndex] ? ii : smallestIndex;
        }
        var larger = list[i];
        list[i] = list[smallestIndex];
        list[smallestIndex] = larger;

    }
    return list;
}

var listArray = [3,15,1,5];
var result = sortArray(listArray);

console.log(result);

 

Better approach:

Merge sort algorithm.

Let’s consider this example with adding more numbers: [3,15,1,5,4,9]

3,15
1,5
--------
9,4


1,3,5,15	| 1
4,9			

3,5,15		| 1,3
4,9			

5,15		
4,9		| 1,3,4

5,15
9		| 1,3,4,5

15
9		| 1,3,4,5,9

15		| 1,3,4,5,9,15

coming soon…

[tabbyending]