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"
}
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
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:
// 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
}
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.setStoreId(storeId = STORE_ID_IN_HOST_APP)
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.
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")
}
}
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()
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)
}
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
Now, to properly finish the Mealz session, you'll need to call the below function:
import ai.mealz.core.Mealz
Mealz.basket.handlePayment(totalAmount: <#T##Double#>)
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 🥳