From bd290a5f6f8876d69fc2c4996aaf6cdc9b224e49 Mon Sep 17 00:00:00 2001 From: Najm Sheikh Date: Mon, 10 Aug 2020 13:21:29 -0400 Subject: [PATCH 1/6] Add public user activity reporting API --- .../usebutton/merchant/ButtonMerchant.java | 10 ++ .../com/usebutton/merchant/ButtonProduct.java | 137 ++++++++++++++++++ .../merchant/ButtonProductCompatible.java | 92 ++++++++++++ .../merchant/ButtonUserActivityImpl.java | 59 ++++++++ .../merchant/module/ButtonUserActivity.java | 59 ++++++++ .../usebutton/merchant/ButtonProductTest.java | 114 +++++++++++++++ 6 files changed, 471 insertions(+) create mode 100644 button-merchant/src/main/java/com/usebutton/merchant/ButtonProduct.java create mode 100644 button-merchant/src/main/java/com/usebutton/merchant/ButtonProductCompatible.java create mode 100644 button-merchant/src/main/java/com/usebutton/merchant/ButtonUserActivityImpl.java create mode 100644 button-merchant/src/main/java/com/usebutton/merchant/module/ButtonUserActivity.java create mode 100644 button-merchant/src/test/java/com/usebutton/merchant/ButtonProductTest.java diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java index 8615155..6ba95b8 100644 --- a/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java @@ -31,6 +31,7 @@ import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; +import com.usebutton.merchant.module.ButtonUserActivity; import com.usebutton.merchant.module.Features; import java.util.concurrent.Executor; @@ -196,6 +197,15 @@ public static Features features() { return FeaturesImpl.getInstance(); } + /** + * An interface through which user activities can be reported. + * + * @return Button user activity API + */ + public static ButtonUserActivity activity() { + return ButtonUserActivityImpl.getInstance(); + } + private static ButtonRepository getButtonRepository(Context context) { PersistenceManager persistenceManager = PersistenceManagerImpl.getInstance(context.getApplicationContext()); diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonProduct.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonProduct.java new file mode 100644 index 0000000..846be40 --- /dev/null +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonProduct.java @@ -0,0 +1,137 @@ +/* + * ButtonProduct.java + * + * Copyright (c) 2020 Button, Inc. (https://usebutton.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.usebutton.merchant; + +import android.support.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +/** + * A concrete implementation of the {@link ButtonProductCompatible} interface. + */ +public class ButtonProduct implements ButtonProductCompatible { + + @Nullable private String id; + @Nullable private String upc; + @Nullable private List categories; + @Nullable private String name; + @Nullable private String currency; + @Nullable private Integer value; + @Nullable private Integer quantity; + @Nullable private String url; + @Nullable private Map attributes; + + @Override + @Nullable + public String getId() { + return id; + } + + @Override + @Nullable + public String getUpc() { + return upc; + } + + @Override + @Nullable + public List getCategories() { + return categories; + } + + @Override + @Nullable + public String getName() { + return name; + } + + @Override + @Nullable + public String getCurrency() { + return currency; + } + + @Override + @Nullable + public Integer getValue() { + return value; + } + + @Override + @Nullable + public Integer getQuantity() { + return quantity; + } + + @Override + @Nullable + public String getUrl() { + return url; + } + + @Override + @Nullable + public Map getAttributes() { + return attributes; + } + + public void setId(@Nullable String id) { + this.id = id; + } + + public void setUpc(@Nullable String upc) { + this.upc = upc; + } + + public void setCategories(@Nullable List categories) { + this.categories = categories; + } + + public void setName(@Nullable String name) { + this.name = name; + } + + public void setCurrency(@Nullable String currency) { + this.currency = currency; + } + + public void setValue(@Nullable Integer value) { + this.value = value; + } + + public void setQuantity(@Nullable Integer quantity) { + this.quantity = quantity; + } + + public void setUrl(@Nullable String url) { + this.url = url; + } + + public void setAttributes(@Nullable Map attributes) { + this.attributes = attributes; + } +} diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonProductCompatible.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonProductCompatible.java new file mode 100644 index 0000000..7256ec3 --- /dev/null +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonProductCompatible.java @@ -0,0 +1,92 @@ +/* + * ButtonProductCompatible.java + * + * Copyright (c) 2020 Button, Inc. (https://usebutton.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.usebutton.merchant; + +import android.support.annotation.Nullable; + +import java.util.List; +import java.util.Map; + +/** + * An interface that defines the product properties that may be provided when reporting user + * activity. + */ +public interface ButtonProductCompatible { + + /** + * @return the product identifier + */ + @Nullable + String getId(); + + /** + * @return the UPC (Universal Product Code) of the product + */ + @Nullable + String getUpc(); + + /** + * @return a flat array of the names of the categories to which the product belongs + */ + @Nullable + List getCategories(); + + /** + * @return the name of the product + */ + @Nullable + String getName(); + + /** + * @return the ISO-4217 currency code in which the product's value is reported + */ + @Nullable + String getCurrency(); + + /** + * @return the value of the order. Includes any discounts, if applicable. Example: 1234 for $12.34 + */ + @Nullable + Integer getValue(); + + /** + * @return the quantity of the product + */ + @Nullable + Integer getQuantity(); + + /** + * @return the URL of the product + */ + @Nullable + String getUrl(); + + /** + * @return any additional attributes to be included with the product + */ + @Nullable + Map getAttributes(); +} diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonUserActivityImpl.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonUserActivityImpl.java new file mode 100644 index 0000000..4d71fbd --- /dev/null +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonUserActivityImpl.java @@ -0,0 +1,59 @@ +/* + * ButtonUserActivityImpl.java + * + * Copyright (c) 2020 Button, Inc. (https://usebutton.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.usebutton.merchant; + +import android.support.annotation.Nullable; + +import com.usebutton.merchant.module.ButtonUserActivity; + +import java.util.List; + +class ButtonUserActivityImpl implements ButtonUserActivity { + + private static ButtonUserActivity activity; + + static ButtonUserActivity getInstance() { + if (activity == null) { + activity = new ButtonUserActivityImpl(); + } + return activity; + } + + @Override + public void productViewed(@Nullable ButtonProductCompatible product) { + + } + + @Override + public void productAddedToCart(@Nullable ButtonProductCompatible product) { + + } + + @Override + public void cartViewed(@Nullable List products) { + + } +} diff --git a/button-merchant/src/main/java/com/usebutton/merchant/module/ButtonUserActivity.java b/button-merchant/src/main/java/com/usebutton/merchant/module/ButtonUserActivity.java new file mode 100644 index 0000000..a61ba2c --- /dev/null +++ b/button-merchant/src/main/java/com/usebutton/merchant/module/ButtonUserActivity.java @@ -0,0 +1,59 @@ +/* + * ButtonUserActivity.java + * + * Copyright (c) 2020 Button, Inc. (https://usebutton.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.usebutton.merchant.module; + +import android.support.annotation.Nullable; + +import com.usebutton.merchant.ButtonProductCompatible; + +import java.util.List; + +/** + * An interface through which user activities can be reported. + */ +public interface ButtonUserActivity { + + /** + * Report that the user has viewed a product. + * + * @param product the viewed product + */ + void productViewed(@Nullable ButtonProductCompatible product); + + /** + * Report that the user has added a product to their cart. + * + * @param product the added product + */ + void productAddedToCart(@Nullable ButtonProductCompatible product); + + /** + * Report that the user viewed their cart. + * + * @param products the products in the cart + */ + void cartViewed(@Nullable List products); +} diff --git a/button-merchant/src/test/java/com/usebutton/merchant/ButtonProductTest.java b/button-merchant/src/test/java/com/usebutton/merchant/ButtonProductTest.java new file mode 100644 index 0000000..6bd5488 --- /dev/null +++ b/button-merchant/src/test/java/com/usebutton/merchant/ButtonProductTest.java @@ -0,0 +1,114 @@ +/* + * ButtonProductTest.java + * + * Copyright (c) 2020 Button, Inc. (https://usebutton.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.usebutton.merchant; + +import org.junit.Before; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class ButtonProductTest { + + private ButtonProduct product; + + @Before + public void setUp() { + product = new ButtonProduct(); + } + + @Test + public void testGetSetId() { + product.setId("test-id"); + + assertEquals("test-id", ((ButtonProductCompatible) product).getId()); + } + + @Test + public void testGetSetUpc() { + product.setUpc("test-upc"); + + assertEquals("test-upc", ((ButtonProductCompatible) product).getUpc()); + } + + @Test + public void testGetSetCategories() { + List categories = new ArrayList<>(); + categories.add("category-one"); + categories.add("category-two"); + product.setCategories(categories); + + assertEquals(categories, ((ButtonProductCompatible) product).getCategories()); + } + + @Test + public void testGetSetName() { + product.setName("test-name"); + + assertEquals("test-name", ((ButtonProductCompatible) product).getName()); + } + + @Test + public void testGetSetCurrency() { + product.setCurrency("test-code"); + + assertEquals("test-code", ((ButtonProductCompatible) product).getCurrency()); + } + + @Test + public void testGetSetValue() { + product.setValue(123); + + assertEquals(new Integer(123), ((ButtonProductCompatible) product).getValue()); + } + + @Test + public void testGetSetQuantity() { + product.setQuantity(321); + + assertEquals(new Integer(321), ((ButtonProductCompatible) product).getQuantity()); + } + + @Test + public void testGetSetUrl() { + product.setUrl("test-url"); + + assertEquals("test-url", ((ButtonProductCompatible) product).getUrl()); + } + + @Test + public void testGetSetAttributes() { + Map attributes = new HashMap<>(); + attributes.put("key", "value"); + product.setAttributes(attributes); + + assertEquals(attributes, ((ButtonProductCompatible) product).getAttributes()); + } +} \ No newline at end of file From 290c6029f7a760e36e658843ff0c8b9186219564 Mon Sep 17 00:00:00 2001 From: Najm Sheikh Date: Tue, 11 Aug 2020 10:20:21 -0400 Subject: [PATCH 2/6] Capture and report user activity --- .../merchant/ActivityReportingTask.java | 62 +++++ .../com/usebutton/merchant/ButtonApi.java | 5 + .../com/usebutton/merchant/ButtonApiImpl.java | 62 +++++ .../usebutton/merchant/ButtonMerchant.java | 8 +- .../usebutton/merchant/ButtonRepository.java | 4 + .../merchant/ButtonRepositoryImpl.java | 34 ++- .../merchant/ButtonUserActivityImpl.java | 57 +++++ .../merchant/ActivityReportingTaskTest.java | 92 ++++++++ .../usebutton/merchant/ButtonApiImplTest.java | 117 ++++++++++ .../merchant/ButtonMerchantTest.java | 19 ++ .../merchant/ButtonRepositoryImplTest.java | 35 ++- .../merchant/ButtonUserActivityImplTest.java | 215 ++++++++++++++++++ 12 files changed, 694 insertions(+), 16 deletions(-) create mode 100644 button-merchant/src/main/java/com/usebutton/merchant/ActivityReportingTask.java create mode 100644 button-merchant/src/test/java/com/usebutton/merchant/ActivityReportingTaskTest.java create mode 100644 button-merchant/src/test/java/com/usebutton/merchant/ButtonUserActivityImplTest.java diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ActivityReportingTask.java b/button-merchant/src/main/java/com/usebutton/merchant/ActivityReportingTask.java new file mode 100644 index 0000000..a645aea --- /dev/null +++ b/button-merchant/src/main/java/com/usebutton/merchant/ActivityReportingTask.java @@ -0,0 +1,62 @@ +/* + * ActivityReportingTask.java + * + * Copyright (c) 2020 Button, Inc. (https://usebutton.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.usebutton.merchant; + +import android.support.annotation.Nullable; + +import com.usebutton.merchant.module.Features; + +import java.util.List; + +/** + * Asynchronous task used to report user activity to Button. + */ +public class ActivityReportingTask extends Task { + + private final ButtonApi buttonApi; + private final DeviceManager deviceManager; + private final Features features; + private final String activityName; + private final List products; + + public ActivityReportingTask(ButtonApi buttonApi, DeviceManager deviceManager, + Features features, String activityName, List products, + @Nullable Listener listener) { + super(listener); + this.buttonApi = buttonApi; + this.deviceManager = deviceManager; + this.features = features; + this.activityName = activityName; + this.products = products; + } + + @Nullable + @Override + Void execute() throws Exception { + String advertisingId = features.getIncludesIfa() ? deviceManager.getAdvertisingId() : null; + return buttonApi.postActivity(activityName, products, advertisingId); + } +} diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonApi.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonApi.java index b2edd7d..769b514 100644 --- a/button-merchant/src/main/java/com/usebutton/merchant/ButtonApi.java +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonApi.java @@ -54,6 +54,11 @@ PostInstallLink getPendingLink(String applicationId, @Nullable String advertisin Void postOrder(Order order, String applicationId, String sourceToken, @Nullable String advertisingId) throws ButtonNetworkException; + @Nullable + @WorkerThread + Void postActivity(String activityName, List products, + @Nullable String advertisingId) throws ButtonNetworkException; + @Nullable @WorkerThread Void postEvents(List events, @Nullable String advertisingId) diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonApiImpl.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonApiImpl.java index 92aee11..f8d60b6 100644 --- a/button-merchant/src/main/java/com/usebutton/merchant/ButtonApiImpl.java +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonApiImpl.java @@ -202,6 +202,68 @@ public Void postOrder(Order order, String applicationId, String sourceToken, return null; } + @Nullable + @Override + public Void postActivity(String activityName, List products, + @Nullable String advertisingId) throws ButtonNetworkException { + + try { + JSONArray productsArray = new JSONArray(); + for (int i = 0; i < products.size(); i++) { + ButtonProductCompatible product = products.get(i); + List categories = product.getCategories(); + Map attributes = product.getAttributes(); + JSONObject productJson = new JSONObject(); + + // Convert categories list to JSON array + JSONArray categoriesJson = new JSONArray(); + if (categories != null) { + for (int j = 0; j < categories.size(); j++) { + categoriesJson.put(j, categories.get(j)); + } + productJson.put("categories", categoriesJson); + } + + // Convert custom attributes map to JSON object + JSONObject attributesJson = new JSONObject(); + if (attributes != null) { + for (Map.Entry entry : attributes.entrySet()) { + attributesJson.putOpt(entry.getKey(), entry.getValue()); + } + productJson.put("attributes", attributesJson); + } + + // Put product data into a JSON object + productJson.put("id", product.getId()); + productJson.put("upc", product.getUpc()); + productJson.put("name", product.getName()); + productJson.put("currency", product.getCurrency()); + productJson.put("value", product.getValue()); + productJson.put("quantity", product.getQuantity()); + productJson.put("url", product.getUrl()); + + // Add to JSON array of products + productsArray.put(i, productJson); + } + + JSONObject requestBody = new JSONObject(); + requestBody.put("ifa", advertisingId); + requestBody.put("name", activityName); + requestBody.put("products", productsArray); + + ApiRequest apiRequest = new ApiRequest.Builder(ApiRequest.RequestMethod.POST, + "/v1/app/activity") + .setBody(requestBody) + .build(); + connectionManager.executeRequest(apiRequest); + } catch (JSONException e) { + Log.e(TAG, "Error creating request body", e); + throw new ButtonNetworkException(e); + } + + return null; + } + @Nullable @Override public Void postEvents(List events, @Nullable String advertisingId) diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java index 6ba95b8..9d73aab 100644 --- a/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonMerchant.java @@ -50,6 +50,8 @@ private ButtonMerchant() { private static Executor executor = new MainThreadExecutor(); @VisibleForTesting static ButtonInternal buttonInternal = new ButtonInternalImpl(executor); + @VisibleForTesting + static ButtonUserActivity activity = ButtonUserActivityImpl.getInstance(); private static ExecutorService executorService = Executors.newSingleThreadExecutor(); static final String BASE_URL = "https://mobileapi.usebutton.com"; @@ -64,6 +66,7 @@ private ButtonMerchant() { */ public static void configure(@NonNull Context context, @NonNull String applicationId) { buttonInternal.configure(getButtonRepository(context), applicationId); + ((ButtonUserActivityImpl) activity()).flushQueue(getButtonRepository(context)); } /** @@ -203,7 +206,7 @@ public static Features features() { * @return Button user activity API */ public static ButtonUserActivity activity() { - return ButtonUserActivityImpl.getInstance(); + return activity; } private static ButtonRepository getButtonRepository(Context context) { @@ -217,7 +220,8 @@ private static ButtonRepository getButtonRepository(Context context) { ButtonApi buttonApi = ButtonApiImpl.getInstance(connectionManager); - return ButtonRepositoryImpl.getInstance(buttonApi, persistenceManager, executorService); + return ButtonRepositoryImpl.getInstance(buttonApi, deviceManager, features(), + persistenceManager, executorService); } private static DeviceManager getDeviceManager(Context context) { diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonRepository.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonRepository.java index 9e55030..6a820f6 100644 --- a/button-merchant/src/main/java/com/usebutton/merchant/ButtonRepository.java +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonRepository.java @@ -29,6 +29,8 @@ import com.usebutton.merchant.module.Features; +import java.util.List; + /** * Internal data layer interface. */ @@ -56,5 +58,7 @@ void getPendingLink(DeviceManager deviceManager, Features features, void postOrder(Order order, DeviceManager deviceManager, Features features, Task.Listener listener); + void trackActivity(String eventName, List products); + void reportEvent(DeviceManager deviceManager, Features features, Event event); } diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonRepositoryImpl.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonRepositoryImpl.java index 6c3dc6e..fabc636 100644 --- a/button-merchant/src/main/java/com/usebutton/merchant/ButtonRepositoryImpl.java +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonRepositoryImpl.java @@ -45,6 +45,8 @@ final class ButtonRepositoryImpl implements ButtonRepository { private static final String TAG = ButtonRepository.class.getSimpleName(); private final ButtonApi buttonApi; + private final DeviceManager deviceManager; + private final Features features; private final PersistenceManager persistenceManager; private final ExecutorService executorService; @@ -52,20 +54,23 @@ final class ButtonRepositoryImpl implements ButtonRepository { private boolean isConfigured; private List> pendingTasks = new CopyOnWriteArrayList<>(); - static ButtonRepository getInstance(ButtonApi buttonApi, PersistenceManager persistenceManager, + static ButtonRepository getInstance(ButtonApi buttonApi, DeviceManager deviceManager, + Features features, PersistenceManager persistenceManager, ExecutorService executorService) { if (buttonRepository == null) { - buttonRepository = new ButtonRepositoryImpl(buttonApi, persistenceManager, - executorService); + buttonRepository = new ButtonRepositoryImpl(buttonApi, deviceManager, features, + persistenceManager, executorService); } return buttonRepository; } @VisibleForTesting - ButtonRepositoryImpl(ButtonApi buttonApi, PersistenceManager persistenceManager, - ExecutorService executorService) { + ButtonRepositoryImpl(ButtonApi buttonApi, DeviceManager deviceManager, Features features, + PersistenceManager persistenceManager, ExecutorService executorService) { this.buttonApi = buttonApi; + this.deviceManager = deviceManager; + this.features = features; this.persistenceManager = persistenceManager; this.executorService = executorService; } @@ -131,6 +136,25 @@ public void postOrder(Order order, DeviceManager deviceManager, Features feature getSourceToken(), deviceManager, features, new ThreadManager())); } + @Override + public void trackActivity(final String eventName, List products) { + ActivityReportingTask task = new ActivityReportingTask(buttonApi, deviceManager, features, + eventName, products, new Task.Listener() { + @Override + public void onTaskComplete(@Nullable Void object) { + // ignored + } + + @Override + public void onTaskError(Throwable throwable) { + Log.e(TAG, String.format("Error reporting user activity [%s]", eventName), + throwable); + } + }); + + invokeIfConfigured(task); + } + @Override public void reportEvent(DeviceManager deviceManager, Features features, final Event event) { EventReportingTask task = new EventReportingTask(buttonApi, deviceManager, features, diff --git a/button-merchant/src/main/java/com/usebutton/merchant/ButtonUserActivityImpl.java b/button-merchant/src/main/java/com/usebutton/merchant/ButtonUserActivityImpl.java index 4d71fbd..7ae83fe 100644 --- a/button-merchant/src/main/java/com/usebutton/merchant/ButtonUserActivityImpl.java +++ b/button-merchant/src/main/java/com/usebutton/merchant/ButtonUserActivityImpl.java @@ -26,15 +26,28 @@ package com.usebutton.merchant; import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; import com.usebutton.merchant.module.ButtonUserActivity; +import java.util.Collections; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; class ButtonUserActivityImpl implements ButtonUserActivity { + @VisibleForTesting static final String EVENT_PRODUCT_VIEWED = "product-viewed"; + @VisibleForTesting static final String EVENT_ADD_TO_CART = "add-to-cart"; + @VisibleForTesting static final String EVENT_CART_VIEWED = "cart-viewed"; + private static ButtonUserActivity activity; + @Nullable + private ButtonRepository buttonRepository; + // TODO: Move logic to ButtonRepository + @VisibleForTesting + List queuedActivityEvents = new CopyOnWriteArrayList<>(); + static ButtonUserActivity getInstance() { if (activity == null) { activity = new ButtonUserActivityImpl(); @@ -44,16 +57,60 @@ static ButtonUserActivity getInstance() { @Override public void productViewed(@Nullable ButtonProductCompatible product) { + Event event = new Event( + EVENT_PRODUCT_VIEWED, + product != null ? Collections.singletonList(product) + : Collections.emptyList() + ); + trackOrQueueEvent(event); } @Override public void productAddedToCart(@Nullable ButtonProductCompatible product) { + Event event = new Event( + EVENT_ADD_TO_CART, + product != null ? Collections.singletonList(product) + : Collections.emptyList() + ); + trackOrQueueEvent(event); } @Override public void cartViewed(@Nullable List products) { + Event event = new Event( + EVENT_CART_VIEWED, + products != null ? products : Collections.emptyList() + ); + + trackOrQueueEvent(event); + } + + private void trackOrQueueEvent(Event event) { + if (buttonRepository != null) { + buttonRepository.trackActivity(event.name, event.products); + } else { + queuedActivityEvents.add(event); + } + } + void flushQueue(ButtonRepository buttonRepository) { + this.buttonRepository = buttonRepository; + + for (Event event : queuedActivityEvents) { + buttonRepository.trackActivity(event.name, event.products); + } + queuedActivityEvents.clear(); + } + + private static class Event { + private final String name; + private final List products; + + public Event(String name, List products) { + this.name = name; + this.products = products; + } } } diff --git a/button-merchant/src/test/java/com/usebutton/merchant/ActivityReportingTaskTest.java b/button-merchant/src/test/java/com/usebutton/merchant/ActivityReportingTaskTest.java new file mode 100644 index 0000000..f3deb85 --- /dev/null +++ b/button-merchant/src/test/java/com/usebutton/merchant/ActivityReportingTaskTest.java @@ -0,0 +1,92 @@ +/* + * ActivityReportingTaskTest.java + * + * Copyright (c) 2020 Button, Inc. (https://usebutton.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.usebutton.merchant; + +import com.usebutton.merchant.module.Features; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ActivityReportingTaskTest { + + @Mock private ButtonApi buttonApi; + @Mock private DeviceManager deviceManager; + @Mock private Features features; + @Mock private Task.Listener listener; + + private String activityName = "test-activity"; + private List products = new ArrayList<>(); + + private ActivityReportingTask task; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + task = new ActivityReportingTask(buttonApi, deviceManager, features, activityName, + products, listener); + } + + @Test + public void execute_includesIfa_hasAdvertisingId_verifyApiCall() throws Exception { + when(features.getIncludesIfa()).thenReturn(true); + String advertisingId = "valid_advertising_id"; + when(deviceManager.getAdvertisingId()).thenReturn(advertisingId); + + task.execute(); + + verify(buttonApi).postActivity(activityName, products, "valid_advertising_id"); + } + + @Test + public void execute_includesIfa_nullAdvertisingId_verifyNullAdvertisingId() throws Exception { + when(features.getIncludesIfa()).thenReturn(true); + when(deviceManager.getAdvertisingId()).thenReturn(null); + + task.execute(); + + verify(buttonApi).postActivity(eq(activityName), eq(products), (String) isNull()); + } + + @Test + public void execute_doesNotIncludesIfa_verifyNullAdvertisingId() throws Exception { + when(features.getIncludesIfa()).thenReturn(false); + when(deviceManager.getAdvertisingId()).thenReturn(""); + + task.execute(); + + verify(buttonApi).postActivity(eq(activityName), eq(products), (String) isNull()); + } +} \ No newline at end of file diff --git a/button-merchant/src/test/java/com/usebutton/merchant/ButtonApiImplTest.java b/button-merchant/src/test/java/com/usebutton/merchant/ButtonApiImplTest.java index 353c045..6839c5f 100644 --- a/button-merchant/src/test/java/com/usebutton/merchant/ButtonApiImplTest.java +++ b/button-merchant/src/test/java/com/usebutton/merchant/ButtonApiImplTest.java @@ -40,10 +40,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @@ -351,6 +353,121 @@ public void postOrder_invalidEmail_verifyPassthrough() throws Exception { assertEquals(customerEmail, customerJson.getString("email_sha256")); } + @Test + public void postActivity_validateRequest() throws Exception { + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ApiRequest.class); + + buttonApi.postActivity("test-activity", Collections.emptyList(), + null); + verify(connectionManager).executeRequest(argumentCaptor.capture()); + ApiRequest apiRequest = argumentCaptor.getValue(); + + assertEquals("/v1/app/activity", apiRequest.getPath()); + assertEquals(ApiRequest.RequestMethod.POST, apiRequest.getRequestMethod()); + } + + @Test + public void postActivity_noProducts_shouldPostActivity() throws Exception { + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ApiRequest.class); + + buttonApi.postActivity("test-activity", Collections.emptyList(), + null); + verify(connectionManager).executeRequest(argumentCaptor.capture()); + ApiRequest apiRequest = argumentCaptor.getValue(); + JSONObject requestBody = apiRequest.getBody(); + + assertEquals("test-activity", requestBody.getString("name")); + assertEquals(0, requestBody.getJSONArray("products").length()); + assertFalse(requestBody.has("ifa")); + } + + @Test + public void postActivity_multipleProducts_shouldPostActivity() throws Exception { + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ApiRequest.class); + final ButtonProductCompatible productOne = new ButtonProduct() {{ + setId("one"); + }}; + final ButtonProductCompatible productTwo = new ButtonProduct() {{ + setId("two"); + }}; + + buttonApi.postActivity("test-activity", new ArrayList() {{ + add(productOne); + add(productTwo); + }}, null); + verify(connectionManager).executeRequest(argumentCaptor.capture()); + ApiRequest apiRequest = argumentCaptor.getValue(); + JSONObject requestBody = apiRequest.getBody(); + + assertEquals("test-activity", requestBody.getString("name")); + assertEquals(2, requestBody.getJSONArray("products").length()); + assertEquals("one", requestBody.getJSONArray("products").getJSONObject(0).getString("id")); + assertEquals("two", requestBody.getJSONArray("products").getJSONObject(1).getString("id")); + assertFalse(requestBody.has("ifa")); + } + + @Test + public void postActivity_withEmptyProductInfo_shouldPostActivity() throws Exception { + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ApiRequest.class); + ButtonProductCompatible product = new ButtonProduct(); + + buttonApi.postActivity("test-activity", Collections.singletonList(product), null); + verify(connectionManager).executeRequest(argumentCaptor.capture()); + ApiRequest apiRequest = argumentCaptor.getValue(); + JSONObject requestBody = apiRequest.getBody(); + + assertEquals("test-activity", requestBody.getString("name")); + assertEquals(1, requestBody.getJSONArray("products").length()); + assertEquals(0, requestBody.getJSONArray("products").getJSONObject(0).length()); + assertFalse(requestBody.has("ifa")); + } + + @Test + public void postActivity_withCompleteProductInfo_shouldPostActivity() throws Exception { + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ApiRequest.class); + List categories = new ArrayList<>(); + categories.add("cat-one"); + categories.add("cat-two"); + Map attributes = new HashMap<>(); + attributes.put("attr-one", "1"); + attributes.put("attr-two", "2"); + final ButtonProduct product = new ButtonProduct(); + product.setId("test-id"); + product.setUpc("test-upc"); + product.setCategories(categories); + product.setName("test-name"); + product.setCurrency("test-curr"); + product.setQuantity(2); + product.setValue(1234); + product.setUrl("test-url"); + product.setAttributes(attributes); + + buttonApi.postActivity("test-activity", new ArrayList() {{ + add(product); + }}, "test-ifa"); + verify(connectionManager).executeRequest(argumentCaptor.capture()); + ApiRequest apiRequest = argumentCaptor.getValue(); + JSONObject requestBody = apiRequest.getBody(); + + assertEquals("test-ifa", requestBody.getString("ifa")); + assertEquals("test-activity", requestBody.getString("name")); + assertEquals(1, requestBody.getJSONArray("products").length()); + JSONObject productJson = requestBody.getJSONArray("products").getJSONObject(0); + assertEquals("test-id", productJson.getString("id")); + assertEquals("test-upc", productJson.getString("upc")); + assertEquals("test-name", productJson.getString("name")); + assertEquals("test-curr", productJson.getString("currency")); + assertEquals("test-url", productJson.getString("url")); + assertEquals(2, productJson.getInt("quantity")); + assertEquals(1234, productJson.getInt("value")); + assertEquals(2, productJson.getJSONArray("categories").length()); + assertEquals("cat-one", productJson.getJSONArray("categories").getString(0)); + assertEquals("cat-two", productJson.getJSONArray("categories").getString(1)); + assertEquals(2, productJson.getJSONObject("attributes").length()); + assertEquals("1", productJson.getJSONObject("attributes").getString("attr-one")); + assertEquals("2", productJson.getJSONObject("attributes").getString("attr-two")); + } + @Test public void postEvents_singleEvent_shouldReportCorrectly() throws Exception { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ApiRequest.class); diff --git a/button-merchant/src/test/java/com/usebutton/merchant/ButtonMerchantTest.java b/button-merchant/src/test/java/com/usebutton/merchant/ButtonMerchantTest.java index 6333dd6..5db43d9 100644 --- a/button-merchant/src/test/java/com/usebutton/merchant/ButtonMerchantTest.java +++ b/button-merchant/src/test/java/com/usebutton/merchant/ButtonMerchantTest.java @@ -45,6 +45,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -87,6 +88,20 @@ public void configure_verifyButtonInternal() { verify(buttonInternal).configure(any(ButtonRepository.class), eq("valid_application_id")); } + @Test + public void configure_verifyFlushActivityQueue() { + ButtonMerchant.activity = spy(ButtonMerchant.activity); + ButtonProductCompatible product = mock(ButtonProductCompatible.class); + + ButtonMerchant.activity().productViewed(product); + ButtonMerchant.activity().productViewed(product); + ButtonMerchant.activity().productViewed(product); + ButtonMerchant.configure(context, "invalid_application_id"); + + verify((ButtonUserActivityImpl) ButtonMerchant.activity).flushQueue( + any(ButtonRepository.class)); + } + @Test public void trackIncomingIntent_verifyButtonInternal() { ButtonMerchant.trackIncomingIntent(context, mock(Intent.class)); @@ -165,4 +180,8 @@ public void features_verifyInstanceType() { assertTrue(ButtonMerchant.features() instanceof FeaturesImpl); } + @Test + public void activity_verifyInstanceType() { + assertTrue(ButtonMerchant.activity() instanceof ButtonUserActivityImpl); + } } \ No newline at end of file diff --git a/button-merchant/src/test/java/com/usebutton/merchant/ButtonRepositoryImplTest.java b/button-merchant/src/test/java/com/usebutton/merchant/ButtonRepositoryImplTest.java index 06d1ae0..dabe580 100644 --- a/button-merchant/src/test/java/com/usebutton/merchant/ButtonRepositoryImplTest.java +++ b/button-merchant/src/test/java/com/usebutton/merchant/ButtonRepositoryImplTest.java @@ -32,6 +32,7 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.List; import java.util.concurrent.ExecutorService; import static org.junit.Assert.assertEquals; @@ -44,21 +45,19 @@ public class ButtonRepositoryImplTest { - @Mock - ButtonApi buttonApi; - - @Mock - PersistenceManager persistenceManager; - - @Mock - ExecutorService executorService; + @Mock ButtonApi buttonApi; + @Mock DeviceManager deviceManager; + @Mock Features features; + @Mock PersistenceManager persistenceManager; + @Mock ExecutorService executorService; private ButtonRepositoryImpl buttonRepository; @Before public void setUp() { MockitoAnnotations.initMocks(this); - buttonRepository = new ButtonRepositoryImpl(buttonApi, persistenceManager, executorService); + buttonRepository = new ButtonRepositoryImpl(buttonApi, deviceManager, features, + persistenceManager, executorService); } @Test @@ -150,6 +149,24 @@ public void postOrder_executeTask() { verify(executorService).submit(any(PostOrderTask.class)); } + @Test + public void trackActivity_configured_executeTask() { + buttonRepository.setApplicationId("invalid_application_id"); + buttonRepository.trackActivity("test-activity", mock(List.class)); + + verify(executorService).submit(any(ActivityReportingTask.class)); + } + + @Test + public void trackActivity_unConfigured_queueTaskAndExecuteWhenConfigured() { + buttonRepository.trackActivity("test-activity", mock(List.class)); + + verify(executorService, never()).submit(any(EventReportingTask.class)); + buttonRepository.setApplicationId("invalid_application_id"); + + verify(executorService).submit(any(ActivityReportingTask.class)); + } + @Test public void reportEvent_configured_executeTask() { buttonRepository.setApplicationId("invalid_application_id"); diff --git a/button-merchant/src/test/java/com/usebutton/merchant/ButtonUserActivityImplTest.java b/button-merchant/src/test/java/com/usebutton/merchant/ButtonUserActivityImplTest.java new file mode 100644 index 0000000..50b98eb --- /dev/null +++ b/button-merchant/src/test/java/com/usebutton/merchant/ButtonUserActivityImplTest.java @@ -0,0 +1,215 @@ +/* + * ButtonUserActivityImplTest.java + * + * Copyright (c) 2020 Button, Inc. (https://usebutton.com) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +package com.usebutton.merchant; + +import com.usebutton.merchant.module.ButtonUserActivity; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentMatchers; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class ButtonUserActivityImplTest { + + private ButtonRepository buttonRepository; + private ButtonUserActivity activityModule; + + @Before + public void setUp() throws Exception { + buttonRepository = mock(ButtonRepository.class); + activityModule = new ButtonUserActivityImpl(); + } + + @Test + public void productViewed_repositoryUnavailable_shouldQueueActivityEvent() { + activityModule.productViewed(null); + activityModule.productViewed(null); + + assertEquals(2, ((ButtonUserActivityImpl) activityModule).queuedActivityEvents.size()); + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + verify(buttonRepository, times(2)).trackActivity( + eq(ButtonUserActivityImpl.EVENT_PRODUCT_VIEWED), + ArgumentMatchers.anyList() + ); + assertEquals(0, ((ButtonUserActivityImpl) activityModule).queuedActivityEvents.size()); + } + + @Test + public void productViewed_repositoryAvailable_shouldTrackActivity() { + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + activityModule.productViewed(null); + activityModule.productViewed(null); + assertEquals(0, ((ButtonUserActivityImpl) activityModule).queuedActivityEvents.size()); + + verify(buttonRepository, times(2)).trackActivity( + eq(ButtonUserActivityImpl.EVENT_PRODUCT_VIEWED), + ArgumentMatchers.anyList() + ); + } + + @Test + public void productViewed_nullProduct_shouldTrackWithEmptyList() { + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + activityModule.productViewed(null); + + verify(buttonRepository).trackActivity( + eq(ButtonUserActivityImpl.EVENT_PRODUCT_VIEWED), + eq(new ArrayList()) + ); + } + + @Test + public void productViewed_validProduct_shouldTrackWithProductInList() { + ButtonProductCompatible product = new ButtonProduct(); + List products = new ArrayList<>(); + products.add(product); + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + activityModule.productViewed(product); + + verify(buttonRepository).trackActivity(ButtonUserActivityImpl.EVENT_PRODUCT_VIEWED, + products); + } + + @Test + public void productAddedToCart_repositoryUnavailable_shouldQueueActivityEvent() { + activityModule.productAddedToCart(null); + activityModule.productAddedToCart(null); + + assertEquals(2, ((ButtonUserActivityImpl) activityModule).queuedActivityEvents.size()); + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + verify(buttonRepository, times(2)).trackActivity( + eq(ButtonUserActivityImpl.EVENT_ADD_TO_CART), + ArgumentMatchers.anyList() + ); + assertEquals(0, ((ButtonUserActivityImpl) activityModule).queuedActivityEvents.size()); + } + + @Test + public void productAddedToCart_repositoryAvailable_shouldTrackActivity() { + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + activityModule.productAddedToCart(null); + activityModule.productAddedToCart(null); + assertEquals(0, ((ButtonUserActivityImpl) activityModule).queuedActivityEvents.size()); + + verify(buttonRepository, times(2)).trackActivity( + eq(ButtonUserActivityImpl.EVENT_ADD_TO_CART), + ArgumentMatchers.anyList() + ); + } + + @Test + public void productAddedToCart_nullProduct_shouldTrackWithEmptyList() { + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + activityModule.productAddedToCart(null); + + verify(buttonRepository).trackActivity( + eq(ButtonUserActivityImpl.EVENT_ADD_TO_CART), + eq(new ArrayList()) + ); + } + + @Test + public void productAddedToCart_validProduct_shouldTrackWithProductInList() { + ButtonProductCompatible product = new ButtonProduct(); + List products = new ArrayList<>(); + products.add(product); + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + activityModule.productAddedToCart(product); + + verify(buttonRepository).trackActivity(ButtonUserActivityImpl.EVENT_ADD_TO_CART, + products); + } + + @Test + public void cartViewed_repositoryUnavailable_shouldQueueActivityEvent() { + activityModule.cartViewed(null); + activityModule.cartViewed(null); + + assertEquals(2, ((ButtonUserActivityImpl) activityModule).queuedActivityEvents.size()); + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + verify(buttonRepository, times(2)).trackActivity( + eq(ButtonUserActivityImpl.EVENT_CART_VIEWED), + ArgumentMatchers.anyList() + ); + assertEquals(0, ((ButtonUserActivityImpl) activityModule).queuedActivityEvents.size()); + } + + @Test + public void cartViewed_repositoryAvailable_shouldTrackActivity() { + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + activityModule.cartViewed(null); + activityModule.cartViewed(null); + assertEquals(0, ((ButtonUserActivityImpl) activityModule).queuedActivityEvents.size()); + + verify(buttonRepository, times(2)).trackActivity( + eq(ButtonUserActivityImpl.EVENT_CART_VIEWED), + ArgumentMatchers.anyList() + ); + } + + @Test + public void cartViewed_nullProductList_shouldTrackWithEmptyList() { + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + activityModule.cartViewed(null); + + verify(buttonRepository).trackActivity( + eq(ButtonUserActivityImpl.EVENT_CART_VIEWED), + eq(new ArrayList()) + ); + } + + @Test + public void cartViewed_validProductList_shouldTrackWithProductList() { + ButtonProductCompatible product = new ButtonProduct(); + List products = new ArrayList<>(); + products.add(product); + ((ButtonUserActivityImpl) activityModule).flushQueue(buttonRepository); + + activityModule.cartViewed(products); + + verify(buttonRepository).trackActivity(ButtonUserActivityImpl.EVENT_CART_VIEWED, + products); + } +} \ No newline at end of file From a67c2818ac2b476edb94090d8ad71d9762fae99c Mon Sep 17 00:00:00 2001 From: Najm Sheikh Date: Tue, 11 Aug 2020 10:22:09 -0400 Subject: [PATCH 3/6] Update sample app with user activity reporting --- .../merchant/sample/KotlinActivity.kt | 29 ++++++++++ .../merchant/sample/MainActivity.java | 53 +++++++++++++++++++ sample/src/main/res/layout/content_main.xml | 29 ++++++++-- 3 files changed, 107 insertions(+), 4 deletions(-) diff --git a/sample/src/main/java/com/usebutton/merchant/sample/KotlinActivity.kt b/sample/src/main/java/com/usebutton/merchant/sample/KotlinActivity.kt index ec5e0bd..96eac9b 100644 --- a/sample/src/main/java/com/usebutton/merchant/sample/KotlinActivity.kt +++ b/sample/src/main/java/com/usebutton/merchant/sample/KotlinActivity.kt @@ -12,7 +12,10 @@ import android.view.View import android.widget.TextView import android.widget.Toast import com.usebutton.merchant.ButtonMerchant +import com.usebutton.merchant.ButtonProduct +import com.usebutton.merchant.ButtonProductCompatible import com.usebutton.merchant.Order +import com.usebutton.merchant.sample.R.id import java.util.Collections import java.util.Date import java.util.Locale @@ -37,6 +40,7 @@ class KotlinActivity : AppCompatActivity() { checkForPostInstallIntent() initTrackNewIntentButton() initTrackOrderButton() + initActivityButtons(); initClearDataButton() initAttributionTokenListener() initReportOrderButton() @@ -119,6 +123,20 @@ class KotlinActivity : AppCompatActivity() { } } + private fun initActivityButtons() { + findViewById(id.track_product_viewed).setOnClickListener { + ButtonMerchant.activity().productViewed(product) + } + + findViewById(id.track_add_cart).setOnClickListener { + ButtonMerchant.activity().productAddedToCart(product) + } + + findViewById(id.track_cart_viewed).setOnClickListener { + ButtonMerchant.activity().cartViewed(listOf(product, product)) + } + } + private fun initClearDataButton() { findViewById(R.id.clear_all_data).setOnClickListener { ButtonMerchant.clearAllData(this) @@ -147,6 +165,17 @@ class KotlinActivity : AppCompatActivity() { private var TEST_URL = String.format(Locale.getDefault(), "https://example.com/p/123?btn_ref=srctok-abc%d&from_landing=true&from_tracking=false&btn_blargh=blergh&other_param=some_val", Random().nextInt(100000)) + private val product: ButtonProductCompatible = ButtonProduct().apply { + id = "prod-123" + upc = "1234567890" + categories = listOf("daily-deals", "black-friday") + name = "Sample Product" + currency = "USD" + value = 129900 + quantity = 1 + url = "https://www.bestbuy.com/site/samsung-65-class-qled-q70-series-4k-uhd-tv-smart-led-with-hdr/6402399.p?skuId=6402399" + attributes = mapOf(Pair("size", "large"), Pair("in_stock", "true")) + } } override fun onCreateOptionsMenu(menu: Menu): Boolean { diff --git a/sample/src/main/java/com/usebutton/merchant/sample/MainActivity.java b/sample/src/main/java/com/usebutton/merchant/sample/MainActivity.java index c33da84..eebd9ae 100644 --- a/sample/src/main/java/com/usebutton/merchant/sample/MainActivity.java +++ b/sample/src/main/java/com/usebutton/merchant/sample/MainActivity.java @@ -41,13 +41,17 @@ import android.widget.Toast; import com.usebutton.merchant.ButtonMerchant; +import com.usebutton.merchant.ButtonProduct; +import com.usebutton.merchant.ButtonProductCompatible; import com.usebutton.merchant.Order; import com.usebutton.merchant.OrderListener; import com.usebutton.merchant.PostInstallIntentListener; import com.usebutton.merchant.UserActivityListener; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -163,6 +167,30 @@ public void onResult(@Nullable final Throwable t) { } }); + findViewById(R.id.track_product_viewed).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ButtonMerchant.activity().productViewed(getProduct()); + } + }); + + findViewById(R.id.track_add_cart).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + ButtonMerchant.activity().productAddedToCart(getProduct()); + } + }); + + findViewById(R.id.track_cart_viewed).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + List products = new ArrayList<>(); + products.add(getProduct()); + products.add(getProduct()); + ButtonMerchant.activity().cartViewed(products); + } + }); + findViewById(R.id.clear_all_data).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -226,4 +254,29 @@ public boolean onOptionsItemSelected(MenuItem item) { } return super.onOptionsItemSelected(item); } + + private static ButtonProductCompatible getProduct() { + ButtonProduct product = new ButtonProduct(); + + List categories = new ArrayList<>(); + categories.add("daily-deals"); + categories.add("black-friday"); + + Map attributes = new HashMap<>(); + attributes.put("size", "large"); + attributes.put("in_stock", "true"); + + product.setId("prod-123"); + product.setUpc("1234567890"); + product.setCategories(categories); + product.setName("Sample Product"); + product.setCurrency("USD"); + product.setValue(129900); + product.setQuantity(1); + product.setUrl( + "https://www.bestbuy.com/site/samsung-65-class-qled-q70-series-4k-uhd-tv-smart-led-with-hdr/6402399.p?skuId=6402399"); + product.setAttributes(attributes); + + return product; + } } diff --git a/sample/src/main/res/layout/content_main.xml b/sample/src/main/res/layout/content_main.xml index 607a52b..950e87c 100644 --- a/sample/src/main/res/layout/content_main.xml +++ b/sample/src/main/res/layout/content_main.xml @@ -49,6 +49,13 @@ android:textStyle="bold" /> +