Find the Index of the First Occurrence in a String

Table of Contents

Task

Given two strings needle and haystack, return the index of the first occurrence of needle in haystack, or -1 if needle is not part of haystack.

Example 1:

Input:

 haystack = "sadbutsad", needle = "sad"

Output:

 0

Explanation:

 "sad" occurs at index 0 and 6.
The first occurrence is at index 0, so we return 0.

Example 2:

Input:

 haystack = "leetcode", needle = "leeto"

Output:

 -1

Explanation:

 "leeto" did not occur in "leetcode", so we return -1.

 

Constraints:

  • 1 <= haystack.length, needle.length <= 104
  • haystack and needle consist of only lowercase English characters.

this problem was taken from Leetcode

Solution

/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
/**
 * @param {string} haystack
 * @param {string} needle
 * @return {number}
 */
var strStr = function(haystack, needle) {
    var match = 0;

    // Edge case: if needle is an empty string, return 0
    if (needle === "") {
        return 0;
    }

    // Get the lengths of both strings
    const haystackLen = haystack.length;
    const needleleLen = needle.length;
    
    // Iterate through the haystack string
    for (let i = 0; i <= haystackLen - needleleLen; i++) {
        var left = 0;
        while(left < needleleLen && haystack[left + i] == needle[left]) {
            left ++;
        }
        if(left == needleleLen) {
            return i;
        }
    }
    return -1;
};

Explanation:

  1. Edge Case: If needle is an empty string, the function immediately returns 0.
  2. Outer Loop: The outer loop iterates over each character in haystack where there is still enough remaining length to match the needle (i <= haystackLen - needleLen).
  3. Inner Loop: The inner loop checks if the substring of haystack starting at i matches needle. It does this by comparing characters one-by-one.
  4. Match Found: If a match is found (j === needleLen), the starting index i is returned.
  5. No Match: If no match is found by the end of the loop, the function returns -1.

This implementation mimics a basic substring search without using any built-in functions. The time complexity is O(n * m), where n is the length of haystack and m is the length of needle.

3 Sum-closest

Table of Contents

Task

Given an integer array nums of length n and an integer target, find three integers in nums such that the sum is closest to target.

Return the sum of the three integers.

You may assume that each input would have exactly one solution.

 

Example 1:

Input:

 nums = [-1,2,1,-4], target = 1

Output:

 2

Explanation:

 The sum that is closest to the target is 2. (-1 + 2 + 1 = 2).

Example 2:

Input:

 nums = [0,0,0], target = 1

Output:

 0

Explanation:

 The sum that is closest to the target is 0. (0 + 0 + 0 = 0).

Constraints:

  • 3 <= nums.length <= 500
  • -1000 <= nums[i] <= 1000
  • -104 <= target <= 104

this problem was taken from Leetcode

Solution

 

Explanation:

  1. Sorting the array: This is necessary so that we can use the two-pointer technique effectively.
  2. Two pointers: For each element, we use two pointers to explore possible sums by adjusting their positions.
  3. Closest sum: We keep track of the closest sum throughout the iteration and update it whenever we find a sum closer to the target.

Check 3 Sum approach for more details.

function threeSumClosest(nums, target) {
    // Sort the array first
    nums.sort((a, b) => a - b);
    let closestSum = Infinity;

    // Iterate through the array
    for (let i = 0; i < nums.length - 2; i++) {
        let left = i + 1;
        let right = nums.length - 1;

        // Use two pointers to find the best sum
        while (left < right) {
            let currentSum = nums[i] + nums[left] + nums[right];

            // Update the closest sum if needed
            if (Math.abs(currentSum - target) < Math.abs(closestSum - target)) {
                closestSum = currentSum;
            }

            // Move the pointers based on the current sum
            if (currentSum < target) {
                left++;
            } else if (currentSum > target) {
                right--;
            } else {
                // If the exact sum is found, return immediately
                return currentSum;
            }
        }
    }

    return closestSum;
}

 

3Sum

Task

 

Given an integer array nums, return all the triplets [nums[i], nums[j], nums[k]] such that i != ji != k, and j != k, and nums[i] + nums[j] + nums[k] == 0.

Notice that the solution set must not contain duplicate triplets.

 

Example 1:

Input:

 nums = [-1,0,1,2,-1,-4]

Output:

 [[-1,-1,2],[-1,0,1]]

Explanation:

 
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0.
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0.
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0.
The distinct triplets are [-1,0,1] and [-1,-1,2].
Notice that the order of the output and the order of the triplets does not matter.

Example 2:

Input:

 nums = [0,1,1]

Output:

 []

Explanation:

 The only possible triplet does not sum up to 0.

Example 3:

Input:

 nums = [0,0,0]

Output:

 [[0,0,0]]

Explanation:

 The only possible triplet sums up to 0.

this problem was taken from Leetcode

Solution

 

Explanation:

  1. Sorting the Array: The array is first sorted to easily manage duplicates and use a two-pointer approach.
  2. Iterating with a Loop: A loop iterates through the array, fixing one element (nums[i]) and then using a two-pointer approach to find the other two elements (nums[left] and nums[right]).
  3. Avoiding Duplicates: Duplicate values are skipped using continue for the first element and while loops for the second and third elements to ensure the solution set contains only unique triplets.
  4. Two-Pointer Approach: The sum is checked, and pointers are adjusted accordingly to find valid triplets.

Example Usage:

[-1, 0, 1, 2, -1, -4]

This would output:

[[-1, -1, 2], [-1, 0, 1]]

This solution efficiently finds all unique triplets that sum to zero in O(n^2) time complexity.

 

Skipping duplicates in the threeSum algorithm is crucial to ensure that the solution set contains only unique triplets. Here’s a detailed explanation of how duplicates are skipped at different stages:

1. Skipping Duplicates for the First Element (i):

When iterating through the array with the outer loop (for (let i = 0; i < nums.length - 2; i++)), the algorithm checks if the current element nums[i] is the same as the previous element nums[i - 1]. If they are the same, it means that any triplet starting with this element would already have been considered in a previous iteration, so the algorithm skips this iteration.

Code Example:

if (i > 0 && nums[i] === nums[i - 1]) continue;

Explanation:

  • i > 0: Ensures that we don’t check for a previous element when i is 0.
  • nums[i] === nums[i - 1]: If this condition is true, it means nums[i] is a duplicate of the previous element, so the loop skips to the next i using continue.

2. Skipping Duplicates for the Second and Third Elements (left and right):

After fixing the first element nums[i], the algorithm uses two pointers, left and right, to find the other two elements (nums[left] and nums[right]) that, together with nums[i], sum to zero.

Once a valid triplet is found, the algorithm moves both pointers inward but also checks for duplicates by comparing the current elements with the next ones in line. If the next element is the same as the current one, the algorithm skips the next element by advancing the pointer further.

Code Example:

// After finding a triplet
while (left < right && nums[left] === nums[left + 1]) left++;
while (left < right && nums[right] === nums[right - 1]) right--;

Explanation:

  • Left Pointer:
    • while (left < right && nums[left] === nums[left + 1]) left++;
    • This loop skips all duplicate values for nums[left] by incrementing left until it points to a new value.
  • Right Pointer:
    • while (left < right && nums[right] === nums[right - 1]) right--;
    • Similarly, this loop skips all duplicate values for nums[right] by decrementing right until it points to a new value.

Why This is Important:

  • Avoiding Redundant Triplets: Without skipping duplicates, the algorithm would include multiple instances of the same triplet in the result, which is inefficient and incorrect for this problem.
  • Efficiency: Skipping duplicates prevents unnecessary comparisons, speeding up the algorithm.

Example Walkthrough:

Consider the array [-1, 0, 1, 2, -1, -4]:

  1. Sorting: The array becomes [-4, -1, -1, 0, 1, 2].
  2. Iteration with i = 0 (nums[i] = -4):
    • No duplicates for nums[i], proceed with left = 1 and right = 5.
    • No valid triplet is found, move to the next i.
  3. Iteration with i = 1 (nums[i] = -1):
    • Triplet [-1, -1, 2] is found.
    • Skip duplicates: left moves from index 2 to 3 because nums[2] === nums[3].
    • Triplet [-1, 0, 1] is found.
  4. Iteration with i = 2 (nums[i] = -1):
    • Skip this iteration entirely because nums[2] === nums[1].

As a result, only unique triplets are returned: [[-1, -1, 2], [-1, 0, 1]].

 

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
/**
 * @param {number[]} nums
 * @return {number[][]}
 */
var threeSum = function(nums) {
    nums.sort((a, b) =>{ return a - b });
    const result = [];

    for(let i = 0; i < nums.length - 2; i ++) {
        // Skip duplicate values for the first element of the triplet
        if (i > 0 && nums[i] === nums[i - 1]) {
          continue;
        }
        let left = i + 1;
        let right = nums.length - 1;

        while(left < right) {
            const sum = nums[i] + nums[left] + nums[right];
            if(sum === 0) {
                result.push([nums[i], nums[left], nums[right]]);
                 // Skip duplicate values for the second and third elements of the triplet
                while (left < right && nums[left] === nums[left + 1]) {
                  left++;
                }
                while (left < right && nums[right] === nums[right - 1]) {
                  right--;
                }
                left ++;
                right --;
            } else if(sum < 0) {
                left ++;
            } else {
                right --;
            }
        }
    }
    return result;
};

 

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 }

 

 

Using GORM library to access postgreSQL with JSONB field with GoLang

This tutorial demonstrates how to ‘auto migrate’ the DB and how to store and retrieve data  from JSONB field in postgreSQL database using GoLang.

Auto migrating the DB

In the context of Go programming language and GORM (Go Object Relational Mapping) library, automigration is a feature that automatically creates or updates database tables based on the Go struct definitions.

For the purpose of this example, we will create a table with id, username, email and meta data field. The meta data will be a JSONB field. We could use JSON as well but JSONB is stored in binary format, and although insert operations are slower searching is faster.

In general it is recommended to always use JSONB unless we have a real good reason to use JSON. For example JSON preserves formatting and allows for duplicate keys.

But before we could use JSONB with GORM we have to install the package

go get github.com/jackc/pgtype

Create a connection to PostgreSQL database.

Gorm supports different databases but here will do this exercise with PostgreSQL only.

func connectToPostgreSQL() (*gorm.DB, error) {
    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
}

Define the structure that will be used to create the the Users table

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

 

Now let’s create two helper functions that will create a new user:

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

func createUser(db *gorm.DB, user *User) error {
    result := db.Create(user)
    if result.Error != nil {
        return result.Error
    }
    return nil
}

And let’s put it all together:

The entire code

package main

import (
    "log"

    "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 connectToPostgreSQL() (*gorm.DB, error) {
    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, metaData string) User {
    jsonData := pgtype.JSONB{}
    err := jsonData.Set([]byte(metaData))
    if err != nil {
        log.Fatal(err)
    }
    // Create a user
    newUser := User{Username: username, Email: email, Age: 36, MetaData: jsonData}
    err = createUser(db, &newUser)
    if err != nil {
        log.Fatal(err)
    }
    return newUser
}

func createUser(db *gorm.DB, user *User) error {
    result := db.Create(user)
    if result.Error != nil {
        return result.Error
    }
    return nil
}

func getUserByID(db *gorm.DB, userID uint) (*User, error) {
    var user User
    result := db.First(&user, userID)
    if result.Error != nil {
        return nil, result.Error
    }
    return &user, nil
}

func updateUser(db *gorm.DB, user *User) error {
    result := db.Save(user)
    if result.Error != nil {
        return result.Error
    }
    return nil
}

func deleteUser(db *gorm.DB, user *User) error {
    result := db.Delete(user)
    if result.Error != nil {
        return result.Error
    }
    return nil
}

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

func main() {

    db := func() *gorm.DB {
        db, err := connectToPostgreSQL()
        if err != nil {
            log.Fatal(err)
        }
        return db
    }()

    autoMigrateDB(db)

    //CRUD operations

    func() {
        newUser := createuserWithMetaData(db, "Toni", "toni@gmail.com", `{"key": "value", "days":[{"dayOne": "1"}], "user-id": "1"}`)
        log.Println("Created user:", newUser)
    }()

    func() {
        newUser := createuserWithMetaData(db, "John", "john@gmail.com", `{"key": "value two", "days":[{"dayOne": "2"}], "user-id": "2"}`)
        log.Println("Created user:", newUser)
    }()

    func() {
        newUser := createuserWithMetaData(db, "Sam", "sam@gmail.com", `{"key": "value three", "days":[{"dayOne": "3"}], "user-id": "3"}`)
        log.Println("Created user:", newUser)
    }()

    // Query user by ID
    user, err := getUserByID(db, 2)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("User by ID:", user)

    var result User
    db.Where("meta_data->>'user-id' = ?", "2").First(&result)
    log.Println(result)
}

 

Updates

We can use byte type instead of pgtype libray which simplifies the code a bit.

type Users struct {
    ID       uint   `gorm:"primaryKey"`
    Username string `gorm:"unique"`
    MetaData []byte `gorm:"type:jsonb" json:"meta-data"`
}

 

func createuserWithMetaData(db *gorm.DB) bool {
    metaData := "{\"one\":\"1\"}"
    // Create a user
    newUser := Users{
        Username: "TEST 123",
        MetaData: []byte(metaData),
    }

    db.Create(&newUser)

    return true
}

 

Authenticate user with JWT in GoLang

  https://ToniNichev@github.com/ToniNichev/tutorials-golang-authanticate-with-jwt.git

 

Generating JWT for testing

Sign, Verify and decode JWT

Setting up the project

we are going to use Gin Web framework to create simple HTTP server that we could query against, passing JWT in the header and then using secret or public key to validate the signature.

package main

import (
    "github.com/gin-gonic/gin"
)

func AuthMiddleware() gin.HandlerFunc {
    // In a real-world application, you would perform proper authentication here.
    // For the sake of this example, we'll just check if an API key is present.
    return func(c *gin.Context) {
        apiKey := c.GetHeader("X-Auth-Token")
        if apiKey == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
            return
        }
        c.Next()
    }
}

func main() {
    // Create a new Gin router
    router := gin.Default()

    // Public routes (no authentication required)
    public := router.Group("/public")
    {
        public.GET("/info", func(c *gin.Context) {
            c.String(200, "Public information")
        })
        public.GET("/products", func(c *gin.Context) {
            c.String(200, "Public product list")
        })
    }

    // Private routes (require authentication)
    private := router.Group("/private")
    private.Use(AuthMiddleware())
    {
        private.GET("/data", func(c *gin.Context) {
            c.String(200, "Private data accessible after authentication")
        })
        private.POST("/create", func(c *gin.Context) {
            c.String(200, "Create a new resource")
        })
    }

    router.POST("query", AuthMiddleware(), validateSession, returnData)

    // Run the server on port 8080
    router.Run(":8080")
}

We added validateSession middleware that will decode the token and verify the signature.

 

Creating JWT services to decode and validate signature

We are using jwt GoLang library to decode the token, and validate the signature.

There are two ways to encode JWT: using symmetric encryption (meaning that the same secret is used to sign and validate the signature. This is done in retreiveTokenWithSymmetrikKey and retreiveTokenWithAsymmetrikKey is used to validate signature using the public key from the private/public key pair used to sign the token.

 

package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io/ioutil"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
)

type User struct {
    name  string
    email string
}

var user User

type requestBody struct {
    OperationName *string     `json:"operationName"`
    Query         *string     `json:"query"`
    Variables     interface{} `json:"variables"`
}

func validateSession(c *gin.Context) {
    user.name = ""
    user.email = ""
    if c.Request.Body != nil {
        bodyBytes, _ := ioutil.ReadAll(c.Request.Body)
        c.Request.Body.Close()
        c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))

        body := requestBody{}
        if err := json.Unmarshal(bodyBytes, &body); err != nil {
            return
        }

        // extract the token from the headers
        tokenStr := c.Request.Header.Get("X-Auth-Token")

        product := body.Variables.(map[string]interface{})["product"]

        var payload string
        var err error
        if product == "web" {
            payload, err = retreiveTokenWithSymmetrikKey(c, tokenStr)
        } else {
            payload, err = retreiveTokenWithAsymmetrikKey(c, tokenStr)
        }

        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{"error": "Session token signature can't be confirmed!"})
        }

        if payload == "" {
            c.AbortWithStatusJSON(401, gin.H{"error": "Invalid token"})
            return
        }
        c.Next()
    }

}

func retreiveTokenWithSymmetrikKey(c *gin.Context, tokenStr string) (string, error) {
    fmt.Println("retreive Token With Symmetric Key ...")

    tknStr := c.Request.Header.Get("X-Auth-Token")
    secretKey := "itsasecret123"

    token, err := jwt.Parse(tknStr, func(token *jwt.Token) (interface{}, error) {
        return []byte(secretKey), nil
    })

    if err != nil {
        c.AbortWithStatusJSON(401, gin.H{"error": "Session token signature can't be confirmed!"})
        return "", errors.New("session token signature can't be confirmed!")
    } else {
        claims := token.Claims.(jwt.MapClaims)
        fmt.Println("======================================")
        fmt.Println(claims)
        fmt.Println(claims["author"])
        fmt.Println(claims["data"])
        fmt.Println("======================================")
        user.name = claims["author"].(string)
    }
    return "token valid", nil
}

func retreiveTokenWithAsymmetrikKey(c *gin.Context, tokenStr string) (string, error) {

    fmt.Println("retreive Token With Asymmetric Key ...")

    publicKeyPath := "key/public_key.pem"
    keyData, err := ioutil.ReadFile(publicKeyPath)
    if err != nil {
        c.AbortWithStatusJSON(401, gin.H{"error": "Error reading public key"})
        return "", errors.New("error reading public key")
    }

    var parsedToken *jwt.Token

    // parse token
    state, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {

        // ensure signing method is correct
        if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
            c.AbortWithStatusJSON(401, gin.H{"error": "Session token signature can't be confirmed!"})
            return nil, errors.New("unknown signing method")
        }

        parsedToken = token

        // verify
        key, err := jwt.ParseRSAPublicKeyFromPEM([]byte(keyData))
        if err != nil {
            return nil, errors.New("parsing key failed")
        }

        return key, nil
    })

    claims := state.Claims.(jwt.MapClaims)

    fmt.Println("======================================")
    fmt.Println("Header [alg]:", parsedToken.Header["alg"])
    fmt.Println("Header [expiresIn]:", parsedToken.Header["expiresIn"])
    fmt.Println("Claims [author]:", claims["author"])
    fmt.Println("Claims [data]:", claims["data"])
    fmt.Println("======================================")
    user.name = claims["author"].(string)

    if !state.Valid {
        return "", errors.New("verification failed")
    }

    if err != nil {
        return "", errors.New("unknown signing error")
    }

    return "token valid", nil
}

func returnData(c *gin.Context) {
    fmt.Println("Returning data ...")
    c.String(200, user.name)
}

 

 

Making requests with JWT

We are going to use Postman to make a new POST request passing the JWT

Generate JWT

Open the second project: Sign, Verify and decode JWT

Make sure that you comment and uncomment the right type of token that you want to use: asymmetric vs symmetric.
Run the project yarn start end copy the long string printed right after SIGNED JWT.

Create new postman POST request

Open Postman and create new POST request. In the url put http://localhost:8080/query this is where our Gin Web server running.

Add X-Auth-Token JWT

Open header section, and add X-Auth-Token key with the value the JWT copied from Sign, Verify and decode JWT

 

Add query parameters and variables.

We are going to pass dummy parameters just for testing except for product
We are going to use product parameter to distinguish between symmetric and asymmetric tokens.
Let’s assume that our app will except symmetric tokens for web and asymmetric for app so make sure that you will pass the right JWT.
Navigate to the GraphQL section of the request, and add the query and the variables.

query

query GetCustomerReccomendations($customerId: String!, $organization: organization!, $product: String!) {
    getCustomer(customerId: $customerId) {
        customerId
        zipCode
        }
    }
}

variables

{
    "customerId": "2b59f049-04d1-43d5-ac87-8ac62069d932",
    "organization": "nbcnews",
    "product": "app"
}

 

Make the request and check the response

If everything works good, you will see the user name printed in the response.

Sign, Verify and decode JWT

  https://github.com/ToniNichev/tutorials-encodeDecodeJWT

Json Web Token become widely popular for creating data with optional signature and/or optional encryption and payload.

JWTs are a Base64 encoded string with a signature attached to it. JWT components are separated by . The components are:

  • Header: Contains metadata about the token, such as the signing algorithm used.
  • Payload: Contains the claims, which are statements about the subject of the token. For example, a JWT might contain claims about a user’s identity, such as their username and email address, or their authorization to access certain resources.
  • Signature: A digital signature that ensures the integrity of the header and payload. The signature is created using the header and payload and a secret key known only to the issuer.

Example token:

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3IiOiJUb25pIFkgTmljaGV2IiwiaWF0IjoxNzA2MTEzNDc0LCJkYXRhIjoiTmV3IEpXVCBnZW5lcmF0ZWQgYXQgV2VkIEphbiAyNCAyMDI0IDExOjI0OjM0IEdNVC0wNTAwIChFYXN0ZXJuIFN0YW5kYXJkIFRpbWUpIiwiZXhwIjoxNzA2MTU2Njc0LCJhdWQiOiJodHRwczovL215c29mdHdhcmUtY29ycC5jb20iLCJpc3MiOiJUb25pIE5pY2hldiIsInN1YiI6InRvbmkubmljaGV2QGdtYWlsLmNvbSJ9.YVDqPvei911_KpPjywiZzzK4vNZAm0wiFC0jMV3qI8eUIuPsJC48GkhjNQFgG3GIqHvkwuWmmZEmpD6UrrxENtw9M8h-iLG9syWMJh1HqsyfpKzdATr3PY7fGE1W9If9v0ULWT7ogO_dMuquEf1vi1PcdW-YjrMqZtSnPbIrgaHogeFd3Hix2Bdmlf8v2TX9CWZHJYbgcTj9xDKFw92GkPgeuqYZ2I0C_2VbsWAjLWmdG5iOQakY7XS2I39qCCd87JLsxXHTfmK4mpMBIUgyOaBIy-o7kfQ1hU5wb-DA0H-GtG-WAgyfpIfw0kgULxV-paVVXQLurv78Lm7x6k5B1g

Let’s do base64 decode on each part of the token above:

Header

echo 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9' |base64 -d
{"alg":"RS256","typ":"JWT"}%
  • “alg”: “RS256” – the encrypting algorithm
  • “typ”: “JWT” – type of the token

Payload

echo 'eyJhdXRob3IiOiJUb25pIFkgTmljaGV2IiwiaWF0IjoxNzA2MTEzNDc0LCJkYXRhIjoiTmV3IEpXVCBnZW5lcmF0ZWQgYXQgV2VkIEphbiAyNCAyMDI0IDExOjI0OjM0IEdNVC0wNTAwIChFYXN0ZXJuIFN0YW5kYXJkIFRpbWUpIiwiZXhwIjoxNzA2MTU2Njc0LCJhdWQiOiJodHRwczovL215c29mdHdhcmUtY29ycC5jb20iLCJpc3MiOiJUb25pIE5pY2hldiIsInN1YiI6InRvbmkubmljaGV2QGdtYWlsLmNvbSJ9' |base64 -d
{"author":"Toni Y Nichev","iat":1706113474,"data":"New JWT generated at Wed Jan 24 2024 11:24:34 GMT-0500 (Eastern Standard Time)","exp":1706156674,"aud":"https://mysoftware-corp.com","iss":"Toni Nichev","sub":"toni.nichev@gmail.com"}

Signature

echo 'YVDqPvei911_KpPjywiZzzK4vNZAm0wiFC0jMV3qI8eUIuPsJC48GkhjNQFgG3GIqHvkwuWmmZEmpD6UrrxENtw9M8h-iLG9syWMJh1HqsyfpKzdATr3PY7fGE1W9If9v0ULWT7ogO_dMuquEf1vi1PcdW-YjrMqZtSnPbIrgaHogeFd3Hix2Bdmlf8v2TX9CWZHJYbgcTj9xDKFw92GkPgeuqYZ2I0C_2VbsWAjLWmdG5iOQakY7XS2I39qCCd87JLsxXHTfmK4mpMBIUgyOaBIy-o7kfQ1hU5wb-DA0H-GtG-WAgyfpIfw0kgULxV-paVVXQLurv78Lm7x6k5B1g' |base64 -d
aP�>���]*����2���@�L"-#1]�#ǔ"��$.<Hc5`��{��妙�&�>���D6�=3�~����%�&G�̟���:�=��MV���E
                                                                                  Y>���2��o�S�uo���*fԧ=�+����]�x��f��/�5�       fG%��q8��2��݆����؍�e[�`#-i��A��t�#'|���q�~b���!H29�H��;��5�Npo�����o�
                                                                                                                                                                                                    �����H/~��U]���.n��NA%

Obviously there is no readable text here.

As we see JWT payload is not encrypted and could be decoded with any base64 decoder so never store sensitive data there. The purpose of signing with our private key is to make sure that ‘audience’ (who ever is going to use the token) will be able to verify the authenticity of this token with shared public key.

Claims to Verify

When code is presented with a JWT, it should verify certain claims. At a minimum, these claims should be checked out:

  • iss identifies the issuer of the JWT. (UUID, domain name, URL or something else)
  • aud identifies the audience of the token, that is, who should be consuming it. aud may be a scalar or an array value.
  • nbf and exp. These claims determine the timeframe for which the token is valid.

It doesn’t matter exactly what this strings are as long as the issuer and consumer of the JWT agree on the values.

JWT signing algorithms.

The default algorithm used is (HS256) which is symmetric: meaning that the same ‘secret’ is used for signing and verifying. In the example below  `itsasecret123`

Symmetric algorithm

jwt-services-symmetric.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)

const secret = 'itsasecret123';

const sign = async (signData, payload) => {
  // Create the JWT header and payload
  const header = {
    'alg': 'RS256',
    'typ': 'JWT'
  };


  const token = jwt.sign(payload, secret);
  return token;
}

const verify = async (token, signData) => {

  try {
    return jwt.verify(token, secret);
  } catch (err) {
    console.log("Error: ", err);
    return false;
  }

}

const decode = async (token) => {
  return jwt.decode(token, {complete: true});
}
export default {
  sign,
  verify,
  decode,
}

 

Asymmetric algorithm

With asymmetric algorithms like (RS256) we use private key to sign the token, and public key to verify the signature. 

How to create public/private key pair:

using opensssl:

  1. Generate the Key Pair:
    openssl genrsa -out private_key.pem 2048
  2. Extract the Public Key
    openssl rsa -in private_key.pem -pubout -out public_key.pem
  3. Secure the Private Key with passphrase (optional but highly recommended)
    openssl rsa -aes256 -in private_key.pem -out private_key_protected.pem

jwt-services-asymmetric.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)

const privateKey = fs.readFileSync('./keys/private_key.pem');
const publicKey = fs.readFileSync('./keys/public_key.pem');

const sign = async (signData, payload) => {
  // Create the JWT header and payload
  const header = {
    'alg': 'RS256',
    'typ': 'JWT'
  };


  // SIGNING OPTIONS
  const signOptions = {
    issuer: signData.issuer,
    subject: signData.subject,
    audience: signData.audience,
    expiresIn: signData.expiresIn,
    algorithm: signData.algorithm,
  };


  const token = jwt.sign(payload, privateKey, signOptions);
  return token;
}

const verify = async (token, signData) => {
  // VERIFY OPTIONS
  const verifyOptions = {
    issuer: signData.issuer,
    subject: signData.subject,
    audience: signData.audience,
    expiresIn: signData.expiresIn,
    algorithm: signData.algorithm,
  };

  try {
    return jwt.verify(token, publicKey, verifyOptions);
  } catch (err) {
    console.log("Error: ", err);
    return false;
  }

}

const decode = async (token) => {
  return jwt.decode(token, {complete: true});
}
export default {
  sign,
  verify,
  decode,
}

Calling the services. Uncomment jwt-services-symmetric .js and comment the other one if you want to test the symmetric JWT sign.

index.js

import jwt from "./jwt-services-asymmetric.js";
//import jwt from "./jwt-services-symmetric.js";

const now = Math.round(new Date().getTime() / 1000);
const secret = "12345";

const signData = {
    issuer: 'Toni Nichev',
    subject: 'toni.nichev@gmail.com',
    audience: 'https://mysoftware-corp.com',
    expiresIn: "12h",
    algorithm: "RS256"
}

const date = new Date();
let dateStr = date.toString();

const payload = {
    "author": "Toni Y Nichev",
    "iat": now,
    "data": `New JWT generated at ${dateStr}`,
};


const token = await jwt.sign(signData, payload);
console.log(`\n==================\nSIGN JWT\n==================\n ${token}`);



const v = await jwt.verify(token, signData);
console.log(`\n==================\nVERIFY SIGNATURE\n==================\n`, v);


const d = await jwt.decode(token);
console.log(`\n==================\nDECODE\n==================\n`, d);

 

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

 

When testing in the sandbox, subscription durations are shortened to help simulate different billing periods quickly:

  • 1 week subscription = 3 minutes in sandbox
  • 1 month subscription = 5 minutes in sandbox
  • 3 months subscription = 15 minutes in sandbox
  • 6 months subscription = 30 minutes in sandbox
  • 1 year subscription = 1 hour in sandbox

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. Download AppleRootCA-G3.cer
  2. Convert certificate to PEM to use it in the PHP script below:
    On MacOS you could navigate to the downloaded certificate and execute:

    openssl x509 -in AppleRootCA-G3.cer -out apple_root.pem
  3. Copy the newly created PEM file to the root folder of the backend server handler, under ./assets folder.
  4. 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);
    
    file_put_contents("./beep.txt", PHP_EOL . $data . PHP_EOL, FILE_APPEND);
    
    $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.

    Now it’s a good time to testing with curl command and make sure that the script reads POST body data.

    curl -X POST -H "Content-Type: application/json" -d '{"key":"value"}' https://yourserver.com/app-store-server-notification-tutorial/

    After executing the command look at ./beep.txt file for the raw response. If you see it, you are ready to continue with a real test notification from Apple server.

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

 

Once server is set up you could issue test notification by following this tutorial from Apple.

If test notification is successful too, you could finally issue a real notification by doing test purchase from the app.

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
    }

     

Adding Google sign-in in iOS with SwiftUI

 

 Git Hub Repo

Google also has a great tutorial here: Get started with Google Sign-In for iOS and macOS

Pre-requirements:

Create authorization credentials.

This is covered in another article here.

Install GoogleSignIn and GoogleSignInWithSwiftSupport

Covered here

Short cheatsheet below:

  1. If you don’t already have CocoaPods installed, follow the steps in the CocoaPods Getting Started guide.
  2. Open a terminal window and navigate to the location of your app’s Xcode project.
  3. If you have not already created a Podfile for your application, create one now:
    pod init
  4. Open the Podfile created for your application and add the following:
    pod 'GoogleSignIn'
  5. If you are using SwiftUI, also add the pod extension for the “Sign in with Google” button:
    pod 'GoogleSignInSwiftSupport'
  6. Save the file and run:
    pod install
  7. From now on Open the generated .xcworkspace workspace file for your application in Xcode. Use this file for all future development on your application. (Note that this is different from the included .xcodeproj project file, which would result in build errors when opened.)
  8. Now we are almost ready to start coding, but when we build the project we might (or might not depends of X-code version) face some issues..

Fixing error rsync.samba(4644) deny(1) file-write-create

Navigate to the Build Settings, find ‘User Script Sandboxing’ and

Flip it to No

Fixing “Your app is missing support for the following URL schemes:”

Copy missing scheme from the error message and add it in the info->url section

 

Let’s get started

 

Adding Google Client ID (GIDClientID)

Ether you face the problems before or not this is one thing that is mandatory.

 

1. Adding UserAuthModel to share between all views.

If you don’t know how to do this read about ObservableObject and @Published and sharing data between Views.

This class has to conform to the ObservableObject in order to have its properties reflecting the View.
We will create methods to check if user is signed in, and update shared parameters: givenName, userEmail, isLoggedIn …

import SwiftUI
import GoogleSignIn
import GoogleSignInSwift

final class UserAuthModel: ObservableObject {
    @Published var givenName: String = ""
    @Published var isLoggedIn: Bool = false
    @Published var errorMessage: String = ""
    @Published var userEmail: String = ""
    @Published var profilePicUrl: String = ""
    
    init() {
        check()
    }
    
    func getUserStatus() {
        if GIDSignIn.sharedInstance.currentUser != nil {
            let user = GIDSignIn.sharedInstance.currentUser
            guard let user = user else { return }
            let givenName = user.profile?.givenName
            self.givenName = givenName ?? ""
            self.userEmail = user.profile!.email
            self.profilePicUrl = user.profile!.imageURL(withDimension: 100)!.absoluteString
            
            self.isLoggedIn = true
        } else {
            self.isLoggedIn = false
            self.givenName = "Not Logged In"
        }
    }
    
    func check() {
        GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
            if let error = error {
                self.errorMessage = "error: \(error.localizedDescription)"
            }
            self.getUserStatus()
            
        }
    }
    
    func gertRootViewController() -> UIViewController {
        guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
            return .init()
        }
        guard let root = screen.windows.first?.rootViewController else {
            return .init()
        }
        return root
    }
    
    
    func signIn() {
        GIDSignIn.sharedInstance.signIn(withPresenting: gertRootViewController()) { signInResult, error in
            guard let result = signInResult else {
                // Inspect error
                print("Error occured in signIn()")
                return
            }
            print("Signing in ...")
            print(result.user.profile?.givenName ?? "")
            self.getUserStatus()
        }
    }
    
    func signOut() {
        GIDSignIn.sharedInstance.signOut()
        self.getUserStatus()
    }
    

 

Now let’s edit the app starter and put  userAuthModel in the environmentObject

//
//  SignInWithGoogleTutorialApp.swift
//  SignInWithGoogleTutorial
//
//  Created by Toni Nichev on 1/3/24.
//

import SwiftUI

@main
struct SignInWithGoogleTutorialApp: App {
    
    @StateObject var userAuthModel: UserAuthModel = UserAuthModel()
    
    var body: some Scene {
        WindowGroup {
            NavigationView {
                ContentView()
            }
            .environmentObject(userAuthModel)
        }
    }
}

 

Adding Sign In / Sign Out buttons to the View

//
//  ContentView.swift
//  SignInWithGoogleTutorial
//
//  Created by Toni Nichev on 1/3/24.
//

import SwiftUI

struct ContentView: View {
    
    @EnvironmentObject var userAuthModel: UserAuthModel
    
    fileprivate func signInButton() -> some View {
        HStack {
            Image("GoogleSignInButton")
                .resizable()
                .frame(width: 50, height: 50)
            
            Button(action: {
                userAuthModel.signIn()
            }, label: {
                Text("Sign In")
            })
        }
    }
    
    fileprivate func signOutButton() -> Button<Text> {
        Button(action: {
            userAuthModel.signOut()
        }, label: {
            Text("Sign Out")
        })
    }
    
    fileprivate func profilePic() -> some View {
        AsyncImage(url: URL(string: userAuthModel.profilePicUrl))
            .frame(width: 100,height: 100)
    }
    
    var body: some View {
        VStack {
            if userAuthModel.isLoggedIn {
                profilePic()
                Text("Hello: \(userAuthModel.givenName)")
                signOutButton()
            } else {
                signInButton()
            }
            
        }
    }
}

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

We have to also edit the #Preview and add userAuthModel there so the preview won’t break.

Adding Authentication with a backend server

Google article

The purpose of authentication on the backend server is to make sure that logged-in users could have access to some protected content, like subscriptions, pro-articles, etc.

Once the user signs-in in the native app, the app sends the id-token to the backend, and the backend validates the token and could return access-token back to the app.

In the previous chapter we added UserAuthModel.swift file.
This is the place to call the backend server.

func sendTokenToBackendServer() {
    
    let user = GIDSignIn.sharedInstance.currentUser
    guard let user = user else { return }
    let stringToken = user.idToken!.tokenString
    
    
    guard let authData = try? JSONEncoder().encode(["idToken" : stringToken]) else {
        return
    }
    
    let url = URL(string: "https://regexor.net/examples/google-sign-in-server-notification/")!
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    
    
    let task = URLSession.shared.uploadTask(with: request, from: authData) { data, response, error in
        print(response ?? ".")
        // handle response from my backend.
        if error != nil {
            print("Error: \(String(describing: error))")
        }
        
        // Handle the response from the server
        let dataString = String(data: data!, encoding: .utf8)
        print ("got data: \(dataString!)")

    }
    
    task.resume()
}

and the final UserAuthenticationModel.swift will look like this:

import SwiftUI
import GoogleSignIn
import GoogleSignInSwift

final class UserAuthModel: ObservableObject {
    @Published var givenName: String = ""
    @Published var isLoggedIn: Bool = false
    @Published var errorMessage: String = ""
    @Published var userEmail: String = ""
    @Published var profilePicUrl: String = ""
    
    
    init() {
        check()
    }
    
    func getUserStatus() {
        if GIDSignIn.sharedInstance.currentUser != nil {
            let user = GIDSignIn.sharedInstance.currentUser
            guard let user = user else { return }
            let givenName = user.profile?.givenName
            self.givenName = givenName ?? ""
            self.userEmail = user.profile!.email
            self.profilePicUrl = user.profile!.imageURL(withDimension: 100)!.absoluteString
            
            self.isLoggedIn = true
        } else {
            self.isLoggedIn = false
            self.givenName = "Not Logged In"
        }
    }
    
    func check() {
        GIDSignIn.sharedInstance.restorePreviousSignIn { user, error in
            if let error = error {
                self.errorMessage = "error: \(error.localizedDescription)"
            }
            self.getUserStatus()
            
        }
    }
    
    func gertRootViewController() -> UIViewController {
        guard let screen = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
            return .init()
        }
        guard let root = screen.windows.first?.rootViewController else {
            return .init()
        }
        return root
    }
    
    
    func signIn() {
        GIDSignIn.sharedInstance.signIn(withPresenting: gertRootViewController()) { signInResult, error in
            guard let result = signInResult else {
                // Inspect error
                print("Error occured in signIn()")
                return
            }
            print("Signing in ...")
            print(result.user.profile?.givenName ?? "")
            self.getUserStatus()
            self.sendTokenToBackendServer()
        }
    }
    
    func signOut() {
        GIDSignIn.sharedInstance.signOut()
        self.getUserStatus()
    }
    
    func sendTokenToBackendServer() {
        
        let user = GIDSignIn.sharedInstance.currentUser
        guard let user = user else { return }
        let stringToken = user.idToken!.tokenString
        
        
        guard let authData = try? JSONEncoder().encode(["idToken" : stringToken]) else {
            return
        }
        
        let url = URL(string: "https://regexor.net/examples/google-sign-in-server-notification/")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        
        let task = URLSession.shared.uploadTask(with: request, from: authData) { data, response, error in
            print(response ?? ".")
            // handle response from my backend.
            if error != nil {
                print("Error: \(String(describing: error))")
            }
            
            // Handle the response from the server
            let dataString = String(data: data!, encoding: .utf8)
            print ("got data: \(dataString!)")

        }
        
        task.resume()
    }
}

 

Server script to get idToken form the native app:
In the example below we Just save the token to a file. In real life scenario, here we have to verify the identity of the id token before sending the access-token back to the app.

<?php

// SAVE RAW DATA
$appleData = file_get_contents('php://input');
// Just saves the token to a file.
// In real life scenario, here we have to verify the identity of the id token before sending the access-token back to the app
$file = fopen("./data.txt", "a");
fwrite($file, $appleData);
fclose($file);

echo "send something back to the native app like acccess-token";

 

Using EnvironmentObject to share data between views

//
//  ContentView.swift
//  Test
//
//  Created by Toni Nichev on 1/3/24.
//

import SwiftUI


// Our observable object class
class GameSettings: ObservableObject {
    @Published var scoree = 0
    var test = 4
}

// A view that expects to find a GameSettings object
// in the environment, and shows its score.
struct ScoreView: View {
    // 2: We are not instantiating gameSetting here since it's already done in ContentView. 
    @EnvironmentObject var gameSettings: GameSettings
    
    var body: some View {
        Text("Score: \(gameSettings.scoree)")
        Text("Test: \(gameSettings.test)")
    }
}

struct ContentView: View {
    // 1: We instantiate GameSettings only here and pass it to the environmentObject at the end
    @StateObject var gameSettings = GameSettings()
    var body: some View {
        NavigationStack {
            VStack {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint)
                
                Button("Increase score") {
                    gameSettings.scoree += 1
                    gameSettings.test += 1
                }
                
                NavigationLink {
                    ScoreView()
                } label: {
                    Text("Show score")
                }
            }
        }
        .environmentObject(gameSettings)
    }
}

#Preview {
    ContentView()
}

#Preview {
    ScoreView().environmentObject(GameSettings())
}