Skip to main content
Version: 5.6

Supplier Minimum requirement

You are a retailer and you have your own groceries list that need to be synchronized with the Mealz groceries list.

SupplierKey - base64 key to set information

This will be provided by Mealz during implementation. This will include information such as PROD vs UAT & your unique company identifier.

If you just want to play around, you can use the sample Mealz Store:

val supplierKey = "ewoJInN1cHBsaWVyX2lkIjogIjE0IiwKCSJwbGF1c2libGVfZG9tYWluZSI6ICJtaWFtLnRlc3QiLAoJIm1pYW1fb3JpZ2luIjogIm1pYW0iLAoJIm9yaWdpbiI6ICJtaWFtIiwKCSJtaWFtX2Vudmlyb25tZW50IjogIlVBVCIKfQ=="

which you'll see decodes from base 64 to this:

{
"supplier_id": "14",
"plausible_domaine": "miam.test",
"miam_origin": "miam",
"origin": "miam",
"miam_environment": "UAT"
}
tip

Your keys will be provided by our Development team

Basic implementation

There are several options to configure to handle authless users. Here is a basic implemtation where the Mealz initialization process will only start after the user is logged in and has selected a valid store. It will then need to complete the basket synchronization

First to init Mealz we need your application context and also your supplier key

note

Two key have been send to you: one for developement and one for production.

import ai.mealz.core.Mealz
import ai.mealz.core.init.sdkRequirement
import android.content.Context

object MealzManager {

private var isInitialized = false
private val SUPPLIER_KEY = "YOUR_KEY"

public fun initialize(applicationContext: Context) {
if (isInitialized) return
Mealz.Core {
sdkRequirement {
key = SUPPLIER_KEY
context = applicationContext
}
}
isInitialized = true
}
}

User setup

Here is how to pass the user ID to the SDK, directly within the host app:

info

You can also enable authless feature, more informations here.

// existingUserId is your user id, type String is expected
Mealz.user.updateUserId(userId = existingUserId)

Here is an example on how to inform the SDK whenever the user login state changes. We recommend using Observables or EventListeners to that end.

import ai.mealz.core.Mealz

class MealzAuth() {
init {
// CODE

OBSERVABLE_ON_USER_OBJECT.collect { user ->
Mealz.shared.user.updateUserId(userId = user.id)
}
}

// CODE
}
info

To get full list of user feature check User configurations.

Store setup

For Mealz to work properly, your user must be connected to a specific store so we can accurately provide recipes with available ingredients. To add the store that the user is currently at, you can use this code:

// From anywhere
import ai.mealz.core.Mealz

// STORE_ID_IN_HOST_APP is your store id, type String is expected
Mealz.user.setStoreIdWithCallBack(storeId = STORE_ID_IN_HOST_APP) { /** action to execute one mealz store has been fetch and set can be a redirection */}

Basket synchronization Setup

Most importantly, the Mealz Basket must be kept up to date with your in-app basket. The SDK handles this complex logic by translating between Mealz Products & your real products. This mechanism is mandatory -> otherwise products will not be able to be added nor deleted properly between the two stores.

For example, when a user adds a Mealz Recipe to their cart, they are adding Mealz products to their basket. We then send them to you, the retailer, to be translated into your products & then added to your basket.

Likewise, when a product is deleted from your basket, the retailer, we listen for updates. When we notice a product used in a Mealz Recipe was deleted, we update our basket to reflect that change as well.

tip

If at some point, you feel like products magically disappear from Mealz recipes, or are not removed from the app basket while they should be, this is probably related to this part.

Helper Function to map between products

More than likely, your Product object will be quite different than ours so we have created a bridge model called SupplierProduct. Because of this, we recommend creating a helper function or two to translate between your object & the bridge. This will make your code easier to understand.

BasketSubscriber & BasketListener

We have two protocols BasketSubscriber & BasketListener to coordinate the two baskets together. As the name suggestions, the listeners listens for updates from your basket. The subscriber allows you to listen.

We think it's best to start to create a new class that implements our new protocols. You can copy this & change the name:

class YourNewBasketService(): BasketPublisher, BasketSubscriber, CoroutineScope by CoroutineScope(Dispatchers.Main) {
override var initialValue: List<SupplierProduct>

override fun onBasketUpdate(sendUpdateToSDK: (List<SupplierProduct>) -> Unit) {
TODO("Not yet implemented")
}

override fun receive(event: List<SupplierProduct>) {
TODO("Not yet implemented")
}
}
danger

IMPORTANT: For all upcoming code examples, we will use MyProduct to replicate YOUR product. In your implementation, use your actual Product class.

onBasketUpdate

Now, we need a way to stream the updates from the Mealz Basket to your own. We recommend using an observable. From there, we can sink your basket (in this case, the ExampleState) with the updates from Mealz.

Here's our example:

override fun onBasketUpdate(sendUpdateToSDK: (List<SupplierProduct>) -> Unit) {
launch {
_RetailerBasketSubject.collect { state ->
LogHandler.debug("[New Mealz] push basket update from supplier")
sendUpdateToSDK(state.items.map { SupplierProduct(it.id, it.quantity) })
}
}
}

initialValue

When Mealz is launching, we need to know the state of the current customer basket. Whenever this basket changes, we will refresh the Mealz SDK.

In our implementation, we create an observable that watches the state of your basket & listens for updates. This observable is inside our BasketService sample implementation. Here is our code:

class MyBasketService(): BasketPublisher, BasketSubscriber, CoroutineScope by CoroutineScope(Dispatchers.Main) {

// we observe all the changes to your basket
private val _RetailerBasketSubject: MutableStateFlow<ExampleState> = MutableStateFlow(pretendExampleState)

// we set the initialValue of the BasketService to the items currently in your basket
override var initialValue: List<SupplierProduct> = _RetailerBasketSubject.value.items.map { SupplierProduct(it.id, it.quantity, it.name, it.image) }

So now in our MealzManager or your local DI, we can call:

private val basketService: MyBasketService = MyBasketService()
danger

IMPORTANT: passing an empty list to Mealz SDK on initialization when there are actual products in the basket will lead to unexpected side effects. If this is done, products in the user's basket will appear as removed by the user in the Mealz basket.

receive

The last step of the basketService is to implement receive. This will make sure both baskets are connected & up to date.

You'll need to create a new function, ours is named updateBasketFromExternalSource that accepts a list of SupplierProducts & updates your basket. The function will iterate through the new products, checking if they are in your basket. If yes, they will either delete them (if the quantity is 0), or update their quantity & info. If no, they will be added to your basket.

Here is our example:

fun updateBasketFromExternalSource(products: List<SupplierProduct>) {
launch {
products.forEach { sp ->
val productToUpdateIdx =
_RetailerBasketSubject.value.items.indexOfFirst { it.id == sp.id }

// Product to add to your basket
if (productToUpdateIdx == -1) {
val product = MyProduct(
sp.id,
attributes = ProductAttributes(name = sp.name ?: "a name", image = sp.imageURL ?: "", price = 1.0),
quantity = sp.quantity
)
_RetailerBasketSubject.value.add(product)
} else if (sp.quantity == 0) {
_RetailerBasketSubject.value.removeItem(productToUpdateIdx)
} else {
_RetailerBasketSubject.value.replaceItem(
index = productToUpdateIdx,
newProduct = _RetailerBasketSubject.value.items[productToUpdateIdx].copy(quantity = sp.quantity)
)
}
}
_RetailerBasketSubject.emit(
ExampleState(
_RetailerBasketSubject.value.items,
_RetailerBasketSubject.value.recipeCount
)
)
}
}

and then simply:

override fun receive(event: List<SupplierProduct>) {
updateBasketFromExternalSource(event)
}
danger

Make sure to handle our event in a synchronious way otherwise your basket may emit too early and we might consider missing product as deleted.

If for example:

  • we send you a list of two products to add tomato and beans
  • then you add tomato and emit your new basket
  • we will consider that beans have been removed

Good way to proceed can be to either add products in batches

  • we send you a list of two products to add tomato and beans
  • then you add tomato and beans in the same time then emit your new basket

or

  • we send you a list of two products to add tomato and beans
  • you add tomato without emiting your new basket
  • then add beans then emit your new basket

Putting it all together

import ai.mealz.core.Mealz

object MealzManager {

private var isInitialized = false
private val basketService: MyBasketService = MyBasketService()

public fun initialize(applicationContext: Context) {
Mealz.Core {
sdkRequirement {
key = supplierKey
context = applicationContext
}
subscriptions {
basket {
// Listen to Miam's basket updates
subscribe(basketService)
// Push client basket notifications
register(basketService)
}
}
}
isInitialized = true
}
}

Checkout

There are two functions that need to be called during the checkout experience. Mealz does not collect payment, but we must reset our basket & lodge the transaction to share with your commerical team. Checkout is a crucial part of the Mealz experience.

Payment Started

When your use selects "Buy now", or when the payment tunnel is begun, you must call paymentStarted.

Mealz.basket.paymentStarted(totalAmount = <#T##Double#>, totalProductCount = <#T##Int?#>, clientOrderId = <#T##String?#>)

This will save the payment information on our end & allow us to cross reference with your team for every transaction Mealz is involved with. Internally, we do NOT track payments where the value of products added by Mealz is 0, so you do not need to implement this logic.

Payment Finished

When your payment tunnel is over, & you've returned a "Payment Success", you must call paymentFinished.

danger

It is of crucial importance that you call this function BEFORE clearing your basket and pass the correct values for the totalAmount & totalProductCount. Please ensure these are the values of the transaction, NOT the values after the transaction (probably an empty basket). If you clear your basket, then call this method, all products will be considered as deleted so the ratio of products added by Mealz will not be correct.

Mealz.basket.paymentFinished(totalAmount = <#T##Double#>, totalProductCount = <#T##Int?#>, clientOrderId = <#T##String?#>)

This will clear the basket & lodge the products as purchased, especially with regard to our AI model. The more a recipe is purchased, the more confident we are in the model & products associated. Further, we also can use this data for analytics purposes to see which recipes are most frequently purchased.

This should be called directly before the user pays & is mandatory.

Congratulations, Mealz is good to go 🥳