Appcenter Push: Unable to display notification - xamarin.forms

I am trying to implement a event handler to see if a custom data attribute is present and if so remove some app data. The issue is now that no due to the event delegate i cannot show the notifications that do not match the custom data value.
I need to display the push notification if it doesn't have the custom data key. How can i do this in Xamarin forms?
private void SetupPushNotificationHandle()
{
// This should come before AppCenter.Start() is called
// Avoid duplicate event registration:
if (!AppCenter.Configured)
{
Push.PushNotificationReceived += (sender, e) =>
{
OceanBusiness business = CoreDependencyService.GetBusinessLayer<OceanBusiness>();
// If there is custom data associated with the notification,
// print the entries
if (e.CustomData != null)
{
foreach (var key in e.CustomData.Keys)
{
switch (key)
{
case CoreSettings.ClearData:
{
if(key.ToLower() == "true")
business.RemoveData();
break;
}
}
}
}
};
}
I have seen how to do this in Xamarin.Android but not in Xamarin.iOS. As such answers that do not show how to do both will not address this question.

Related

Updating Azure SBNotificationHub tags after initial registration

I have a Xamarin Forms App which registers for notifications fine for iOS via the AppDelegate and the RegisteredForRemoteNotifications method. I have a tag I register here as well.
I was looking to have user settings to be able to update the tag registered. So as a test, I create a Dependency Service taking the Device Token and tag.
public async void UpdateTags(string token, string tag)
{
NSData deviceToken = new NSData(token, NSDataBase64DecodingOptions.None);
Hub = new SBNotificationHub(Constants.ListenConnectionString, Constants.NotificationHubName);
string[] SubscriptionTags = { tag };
var tags = new NSSet(SubscriptionTags.ToArray());
Hub.UnregisterAll(deviceToken, (error) =>
{
if (error != null)
{
Debug.WriteLine($"Unable to call unregister {error}");
return;
}
Hub.RegisterNative(deviceToken, tags, (errorCallback) =>
{
if (errorCallback != null)
{
Debug.WriteLine($"RegisterNativeAsync error: {errorCallback}");
}
});
var templateExpiration = DateTime.Now.AddDays(120).ToString(System.Globalization.CultureInfo.CreateSpecificCulture("en-US"));
Hub.RegisterTemplate(deviceToken, "defaultTemplate", Constants.APNTemplateBody, templateExpiration, tags, (errorCallback) =>
{
if (errorCallback != null)
{
if (errorCallback != null)
{
Debug.WriteLine($"RegisterTemplateAsync error: {errorCallback}");
}
}
});
});
}
This method runs correctly and the hub referred to here has the updated tags but the app seems to hold on to the old tag.
Do I need to refer to the original Hub created in the RegisteredForRemoteNotifications method in AppDelegate.cs?
Or is this the wrong approach? Long story short I need to update tags created via the RegisteredForRemoteNotifications method in AppDelegate.cs on app load.
Thank you!

Is there a way for a UWP app to know if Recurring Billing is turned on for a subscription in the Microsoft store

I have a Xamarin Forms UWP app. I am trying to provide more specific information to users of my app regarding their subscription expiration / renewal. I would like to be able to tell if they have turned off recurring billing for the subscription. Below are the details I get back from the store regarding the current subscription. I assume the isActive will turn to false after they turn off recurring billing AND cancel the subscription after Expiration date. However, in my testing I have found that if they turn off recurring billing and are still within the the subscription period of their last recurring purchase (i.e. the expiration date is still in the future), everything seems to look exactly as if they have recurring billing turned on and the subscription will auto-renew. I am trying find a way to let them know that their current subscription will expire on a specific date versus will auto-renew on a specific date. Am I missing something?
Here is the code -- Everything works fine I just don't see anything in the object returned from the Microsoft Store API that indicates if recurring billing is on for the subscription.
using FCISharedAll;
using FCISuite.UWP;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Windows.Services.Store;
using static FCISharedAll.FCIEnums.FlexEnums;
[assembly: Xamarin.Forms.Dependency(typeof(UWPSubscriptionManager))]
namespace FCISuite.UWP
{
public class UWPSubscriptionManager : IFCISubcriptionManager, IDisposable
{
private StoreContext context = null;
private static string[] ia_productKinds = { "Durable", "Consumable", "UnmanagedConsumable" };
private List<String> iobj_filterList = null;
//We need a constructor to load the list
public UWPSubscriptionManager()
{
iobj_filterList = new List<string>(ia_productKinds);
}
/// <summary>
/// This will run at the start of the app to get the active subscription info for the user.
/// </summary>
/// <returns></returns>
public async Task<ActiveSubscriptionDetails> GetActiveSubscriptionInformation(ActiveSubscriptionDetails pobj_SubscriptionData)
{
string ls_Result = string.Empty;
try
{
if (context == null)
{
context = StoreContext.GetDefault();
// If your app is a desktop app that uses the Desktop Bridge, you
// may need additional code to configure the StoreContext object.
// For more info, see https://aka.ms/storecontext-for-desktop.
}
StoreAppLicense appLicense = await context.GetAppLicenseAsync();
// Check if the customer has the rights to the subscription.
foreach (var addOnLicense in appLicense.AddOnLicenses)
{
StoreLicense license = addOnLicense.Value;
if (license.IsActive)
{
pobj_SubscriptionData.ActiveSubscriptionID = license.SkuStoreId;
pobj_SubscriptionData.SubscriptionEndDate = license.ExpirationDate.DateTime;
}
}
}
catch (Exception ex)
{
SharedErrorHandler.ProcessException(ex);
}
//Debug.WriteLine("End of Subscription Info");
return pobj_SubscriptionData;
}
#region get available subscriptions
public async Task<List<SubscriptionDetail>> GetAvailableSubscriptions(string ps_ProductNotFoundMessage,
string ps_StoreAddOnsNotFound, string ps_EnvironmentPrefix)
{
string ls_Result = "";
List<SubscriptionDetail> lobj_AvailableSubscriptions = new List<SubscriptionDetail>();
try
{
if (context == null)
{
context = StoreContext.GetDefault();
// If your app is a desktop app that uses the Desktop Bridge, you
// may need additional code to configure the StoreContext object.
// For more info, see https://aka.ms/storecontext-for-desktop.
}
// Get app store product details. Because this might take several moments,
// display a ProgressRing during the operation.
StoreProductResult product_queryResult = await context.GetStoreProductForCurrentAppAsync();
if (product_queryResult.Product == null)
{
// Show additional error info if it is available.
if (product_queryResult.ExtendedError != null)
{
ls_Result += $"\nExtendedError: {product_queryResult.ExtendedError.Message}";
}
// The Store catalog returned an unexpected result.
throw new Exception(ps_ProductNotFoundMessage + " " + ls_Result);
}
else
{
StoreProductQueryResult addon_queryResult = await context.GetAssociatedStoreProductsAsync(iobj_filterList);
if (addon_queryResult.ExtendedError != null)
{
// The user may be offline or there might be some other server failure.
ls_Result = $"ExtendedError: {addon_queryResult.ExtendedError.Message}";
// The Store catalog returned an unexpected result for the Add-ons.
throw new Exception(ps_StoreAddOnsNotFound + " " + ls_Result);
}
List<KeyValuePair<string, StoreProduct>> lobj_ProductsforEnvironment = new List<KeyValuePair<string, StoreProduct>>();
//We are in test - do not show the production subscriptions
lobj_ProductsforEnvironment = (from tobj_Product in addon_queryResult.Products
where tobj_Product.Value.Title.ToUpper().StartsWith(ps_EnvironmentPrefix.ToUpper())
select tobj_Product).ToList();
//foreach (KeyValuePair<string, StoreProduct> item in addon_queryResult.Products)
foreach (KeyValuePair<string, StoreProduct> item in lobj_ProductsforEnvironment)
{
// Add the store product's skus to the list
foreach (StoreSku lobj_Sku in item.Value.Skus)
{
if (lobj_Sku.IsSubscription)
{
lobj_AvailableSubscriptions.Add(new SubscriptionDetail()
{
SubscriptionID = item.Value.StoreId,
SubscriptionName = lobj_Sku.Title,
SubscriptionFormattedPrice = lobj_Sku.Price.FormattedPrice,
// Use the sku.SubscriptionInfo property to get info about the subscription.
// For example, the following code gets the units and duration of the
// subscription billing period.
SubscriptionPeriodUnit = lobj_Sku.SubscriptionInfo.BillingPeriodUnit.ToString(),
SubscriptionPeriod = lobj_Sku.SubscriptionInfo.BillingPeriod,
});
}
}
}
}
}
catch (Exception ex)
{
SharedErrorHandler.ProcessException(ex);
}
return lobj_AvailableSubscriptions;
}
#endregion
#region Subscription Purchase area
/// <summary>
/// This will get the product for the ID from the store, then request to purchase it for the user.
/// </summary>
/// <param name="ps_SubscriptionStoreId">The ID of the store subscription to be purchased</param>
/// <returns>boolean that indicates if the purchase succeeded or failed.</returns>
public async Task<SubscriptionPurchaseDetail> PurchaseSubscription(string ps_SubscriptionStoreId)
{
SubscriptionPurchaseDetail lobj_SubscriptionPurchaseDetail = new SubscriptionPurchaseDetail();
try
{
lobj_SubscriptionPurchaseDetail.PurchaseStatus = FCIStorePurchaseStatus.Succeeded;
lobj_SubscriptionPurchaseDetail.ExtendedErrorMessage = string.Empty;
lobj_SubscriptionPurchaseDetail.ProductToPurchaseWasFound = true;
//This gets the product to purchase
var tobj_StoreProduct = await GetSubscriptionProductAsync(ps_SubscriptionStoreId);
if (tobj_StoreProduct == null)
{
lobj_SubscriptionPurchaseDetail.ExtendedErrorMessage = "The add-on to purchase was not found. Product ID: " + ps_SubscriptionStoreId;
lobj_SubscriptionPurchaseDetail.ProductToPurchaseWasFound = false;
}
else
{
//Get the store product to purchase
StorePurchaseResult result = await tobj_StoreProduct.RequestPurchaseAsync();
//Convert the store namespace string to our string.
lobj_SubscriptionPurchaseDetail.PurchaseStatus = (FCIStorePurchaseStatus)Enum.Parse(typeof(FCIStorePurchaseStatus), result.Status.ToString());
// Capture the error message for the operation, if any.
if (result.ExtendedError != null)
{
lobj_SubscriptionPurchaseDetail.ExtendedErrorMessage += " " + result.ExtendedError.Message;
}
}
}
catch (Exception ex)
{
SharedErrorHandler.ProcessException(ex);
}
return lobj_SubscriptionPurchaseDetail;
}
/// <summary>
/// This returns the product we will use to call the RequestPurchaseAsync function on
/// </summary>
/// <param name="ps_SubscriptionStoreId">The ID if the subscription to purchase</param>
/// <returns>StoreProduct which represents subscritpion to purchase.</returns>
private async Task<StoreProduct> GetSubscriptionProductAsync(string ps_SubscriptionStoreId)
{
try
{
if (context == null)
{
context = StoreContext.GetDefault();
// If your app is a desktop app that uses the Desktop Bridge, you
// may need additional code to configure the StoreContext object.
// For more info, see https://aka.ms/storecontext-for-desktop.
}
// Load the sellable add-ons for this app and check if the trial is still
// available for this customer. If they previously acquired a trial they won't
// be able to get a trial again, and the StoreProduct.Skus property will
// only contain one SKU.
StoreProductQueryResult result = await context.GetAssociatedStoreProductsAsync(iobj_filterList);
if (result.ExtendedError != null)
{
throw new Exception("Something went wrong while getting the add-ons from the store. ExtendedError:" + result.ExtendedError);
}
// Look for the product that represents the subscription and return it if found - this is what we
//will purchase
foreach (var item in result.Products)
{
StoreProduct product = item.Value;
if (product.StoreId == ps_SubscriptionStoreId)
{
return product;
}
}
//If we get here the subscription was not found. We will return null and the calling function will know the
//subscription was not found.
}
catch (Exception ex)
{
SharedErrorHandler.ProcessException(ex);
}
return null;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
context = null;
}
}
#endregion
}
}
Currently, there is no such API from StoreContext that could get the information about whether the user disables auto-renew for the subscription.
When users have purchased a subscription add-on, it will renew automatically by default. Detailed messages are listed here: Subscription renewals and grace periods. If users need to check subscription mode, he still need to check it from the account service site.

Xamarin Forms: How to fetch the latest contacts of phone after adding a number to phonebook?

I have referred to this blog for listing the phone contacts. Also, I have implemented adding contacts to the phonebook using DependencyService as per this thread.
My problem is after adding a contact to the device phone book I need to fetch the entire latest contacts. Also, I need to show the new contact on the contact listview.
For reference, I have created a sample project and uploaded it here. In this sample first of all I am listing the phone contacts with the Add New option on the top. If we tap Add New, a new page will open with the Add to Contact option and entry for phone number. Click the Add to Contact option after entering a phone number, then the device phonebook page will show with the phone number entered.
At this stage, the user may or may not save that number to the device phonebook. So when the user resuming to the App I need to fetch the entire contacts and check the phone number is added or not to the device phone book. If contact added I will hide the Add to Contact option else I will show that option again. At the same time when the user going back to the contact list, I need to show the newly added contact over there.
For this, I have added a message on App.xaml.cs and Subscribe it on AddContactPage.
App.xaml.cs
protected override void OnResume()
{
// Handle when your app resumes
MessagingCenter.Send(this, "isContactAdded");
}
AddContactPage
MessagingCenter.Subscribe<App>(this, "isContactAdded", (sender) =>
{
//How I can fetch the entire latest contacts here
//After fetching conatcts only I can check the new phone number is added to the device phonebook
});
The contacts are passing as an argument from MainActivity to App.xaml.cs. So I don't know how to fetch it directly. Is there any way to fetch all contacts on this page?
//loading all the new contacts
protected async override void OnResume()
{
if (Utility.isContactAdding)
{
UserDialogs.Instance.ShowLoading("");
List<Contact> contacts = await contactsService.RetrieveContactsAsync() as List<Contact>;
MessagingCenter.Send<App, List<Contact>>(App.Current as App, "isContactAdded", contacts);
}
}
//Comparing the new contacts with phone number
MessagingCenter.Subscribe<App, List<Contact>>(App.Current, "isContactAdded", (snd, arg) =>
{
Device.BeginInvokeOnMainThread(() =>
{
Utility.ContactsList.Clear();
phone = Regex.Replace(phone, #"[^0-9+]+", "");
bool isContactExist = false;
var AllNewContacts = arg as List<Contact>;
foreach(var item in AllNewContacts)
{
if (item.PhoneNumbers.Length != 0)
{
foreach(var number in item.PhoneNumbers)
{
if (number.Replace("-", "").Replace(" ","") == phone)
{
isContactExist = true;
}
}
}
Utility.ContactsList.Add(item);
}
if (isContactExist)
{
Phonebook_layout.IsVisible = false;
MessagingCenter.Send<CallHistoryDetailPage>(this, "refreshcontacts");
}
else
{
Phonebook_layout.IsVisible = true;
}
Utility.isContactAdding = false;
UserDialogs.Instance.HideLoading();
});
});
//Subscribed message and refershing the contacts
MessagingCenter.Subscribe<CallHistoryDetailPage>(this, "refreshcontacts", (sender) =>
{
BindingContext = new ContactsViewModel(Utility.myContacts);
});

PushSharp 4.0.10.0: HTTP/2-based Apple Push Notification service (APNs)

We use PushSharp 4.0.10 to send iOS Push Notifications:
https://github.com/Redth/PushSharp
Recently we recieved this email from Apple Developer:
"If you still send push notifications with the legacy binary protocol, it's time to update to the HTTP/2-based Apple Push Notification service (APNs) provider API. You'll be able to take advantage of great features, such as authentication with a JSON Web Token, improved error messaging, and per-notification feedback.
To give you additional time to prepare, the deadline to upgrade to the APNs provider API has been extended to March 31, 2021. We recommend upgrading as soon as possible, as APNs will no longer support the legacy binary protocol after this date."
My question is: Will PushSharp 4.0.10 still work after March 31, 2021?
There is a discussion about this but the thread was closed. But there are still some suggestions on this thread that you might want to try.
The Apple Push Notification service (APNs) will no longer support the legacy binary protocol as of November 2020
https://github.com/Redth/PushSharp/issues/923
**
EDIT - 25th March 2021
The deadline is close and #Ashita Shah asked some code snippet so I hope the following can save your time.
Add the following class dotAPNSService to your project. You can customise this structure according to your needs. Also I didn't focus the best of best coding C# standards when implementing my own push notification service. You can implement LINQ, Tasks async etc. I tested this dotAPNS library and it works perfectly fine. For Android you can still use PushSharp.
Before you implement the dotAPNSService helper class, get the following from your Apple developer account. The ApnsJwtOptions values should be:
BundleId - your app’s bundle ID. Should not include specific topics (i.e. com.myapp but not com.myapp.voip).
CertFilePath - path to the .p8 certificate you have downloaded from the Developer Center.
KeyId - The 10-character Key ID you obtained from your developer account
TeamId - The 10-character Team ID you use for developing your company’s apps. Obtain this value from your developer account.
public class dotAPNSService : IDisposable
{
public event EventHandler OnTokenExpiredHandler;
private ApnsJwtOptions options = null;
public dotAPNSService()
{
options = new ApnsJwtOptions()
{
BundleId = "com.xx.xxxx",
CertFilePath = "../../certificate.p8",
KeyId = "The_Key_Id",
TeamId = "The_Team_Id"
};
}
public void SendNotifications(String[] deviceTokens, String title, String body)
{
if (deviceTokens == null || deviceTokens.Length <= 0)
{
return;
}
if (String.IsNullOrEmpty(title))
{
return;
}
if (String.IsNullOrEmpty(body))
{
return;
}
// once you've gathered all the information needed and created an options instance, it's time to call
var apns = ApnsClient.CreateUsingJwt(new HttpClient(), options);
// start the process
foreach (String deviceToken in deviceTokens)
{
var push = new ApplePush(ApplePushType.Alert)
.AddAlert(title, body)
.AddToken(deviceToken);
Send(apns, push, deviceToken);
}
}
public void SendSilentNotifications(String[] deviceTokens)
{
try
{
if (deviceTokens == null || deviceTokens.Length <= 0)
{
return;
}
// once you've gathered all the information needed and created an options instance, it's time to call
var apns = ApnsClient.CreateUsingJwt(new HttpClient(), options);
// start the process
foreach (String deviceToken in deviceTokens)
{
var push = new ApplePush(ApplePushType.Background)
.AddContentAvailable()
.AddToken(deviceToken);
Send(apns, push, deviceToken);
}
}
finally
{
}
}
private void Send(ApnsClient apns, ApplePush push, String deviceToken)
{
try
{
var response = apns.SendAsync(push);
if (response.Result.Reason == ApnsResponseReason.Success)
{
// the notification has been sent!
}
else
{
Boolean removeToken = false;
switch (response.Result.Reason)
{
case ApnsResponseReason.BadDeviceToken:
removeToken = true;
break;
case ApnsResponseReason.TooManyRequests:
break;
}
// remove the token from database?
if (removeToken)
OnTokenExpired(new ExpiredTokenEventArgs(deviceToken));
}
}
catch (TaskCanceledException)
{
// ERROR - HTTP request timed out, you can use the deviceToken to log the error
}
catch (HttpRequestException ex)
{
// ERROR - HTTP request failed, you can use the deviceToken to log the error
}
}
protected virtual void OnTokenExpired(ExpiredTokenEventArgs args)
{
try
{
EventHandler handler = OnTokenExpiredHandler;
if (handler != null)
{
ISynchronizeInvoke target = handler.Target as ISynchronizeInvoke;
if (target != null && target.InvokeRequired)
target.Invoke(handler, new object[] { this, args });
else
handler(this, args);
}
}
catch (Exception ex)
{
}
}
}
These are the namespaces of the dotAPNSService helper class:
using System;
using System.ComponentModel;
using System.Net.Http;
using System.Threading.Tasks;
using dotAPNS;
In order to use the dotAPNSService helper on your project just pull the tokens from the database and then pass them to it. For instance, to send silent notifications:
public void SendScheduledSilentNotifications()
{
try
{
IList<User> users = _accountService.GetUsers(true);
if (users != null && users.Count > 0)
{
List<String> deviceTokens = new List<String>();
foreach (User user in users)
{
if (!String.IsNullOrEmpty(user.DeviceToken))
deviceTokens.Add(user.DeviceToken);
}
if (deviceTokens.Count > 0)
{
using (dotAPNSService service = new dotAPNSService())
{
service.OnTokenExpiredHandler += new EventHandler(OnTokenExpired);
service.SendSilentNotifications(deviceTokens.ToArray());
}
}
}
}
finally
{
}
}
To remove the expired tokens from the database you can use the following:
private void OnTokenExpired(object sender, EventArgs e)
{
if (e == null)
return;
if (e.GetType() == typeof(ExpiredTokenEventArgs))
{
var args = (ExpiredTokenEventArgs)e;
User user = _accountService.GetUserByDeviceToken(args.Token);
if (user != null)
{
user.DeviceToken = String.Empty;
Boolean success = !(_accountService.SaveUser(user) == null);
if (success)
// INFO - expired device token has been removed from database
else
// INFO - something went wrong
}
}
}
You can download the source code from here:
https://github.com/alexalok/dotAPNS
The API is now sending thousands of silent notifications at one time and there are no delays, crashes etc. Hope this code snippet helps and saves your time!

Xamarin forms callkit integration

I am trying to develop a xamarin forms app in which user can make call
(Navigate to dialer) from taping on number showed on app.In android I accomplished this through dependency service.But in ios I am stuck.I heard about callkit.I saw the documentation of it in https://learn.microsoft.com/en-us/xamarin/ios/platform/callkit?tabs=windows. But how can I actually implement on this in my App? I added all the classes in that document to my app.But how I can make the call from xamal.cs to the ios specified code? By using Dependency service?
Edit: I know how to navigate app to dialer or phone app. Why I am using callkit is I want to get the call duartion.
I created an Instance
public interface IosCallerDialer
{
void StartCall();
}
Implementation on ios
class IosCallDial: IosCallerDialer
{
private CXCallController CallController = new CXCallController();
private void SendTransactionRequest(CXTransaction transaction)
{
// Send request to call controller
CallController.RequestTransaction(transaction, (error) => {
// Was there an error?
if (error == null)
{
// No, report success
Console.WriteLine("Transaction request sent successfully.");
}
else
{
// Yes, report error
Console.WriteLine("Error requesting transaction: {0}", error);
}
});
}
public void StartCall()
{
// Build call action
string contact = "8547085532";
var handle = new CXHandle(CXHandleType.Generic, contact);
var startCallAction = new CXStartCallAction(new NSUuid(), handle);
// Create transaction
var transaction = new CXTransaction(startCallAction);
// Inform system of call request
SendTransactionRequest(transaction);
}
}
My xaml.cs
async void btnCall_Clicked(object sender, System.EventArgs e)
{
DependencyService.Get<IosCallerDialer>().StartCall();
}
Apart this I added all the classes defined in the document.I want only outgoing call. Is this proper way? I cant find any tutorials regarding callkit on xamarin. Any help is appreciated.
EDIT: I understand Callkit only for voip. So is there any other workaround like starting a timer when moves to phone app and stop timer when returns to app? Is it possible? Please provide any insights.
You can try the code below to detect the state of incoming call.
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
//
// This method is invoked when the application has loaded and is ready to run. In this
// method you should instantiate the window, load the UI into it and then make the window
// visible.
//
// You have 17 seconds to return from this method, or iOS will terminate your application.
//
public CTCallCenter c { get; set; }
public override bool FinishedLaunching(UIApplication app, NSDictionary options)
{
global::Xamarin.Forms.Forms.Init();
LoadApplication(new App());
c = new CTCallCenter();
c.CallEventHandler = delegate (CTCall call)
{
if (call.CallState == call.StateIncoming)
{
//start the timer
}
else if (call.CallState == call.StateDialing)
{
}
else if (call.CallState == call.StateConnected)
{
}
else if(call.CallState == call.StateDisconnected)
{
//end the timer
//use messagecenter to send duartion
MessagingCenter.Send<Object>(new Object(), "Hi");
}
};
return base.FinishedLaunching(app, options);
}
}
And any Where in Xamarin.forms:
MessagingCenter.Subscribe<Object>(this, "Hi", (sender) => {
// do something whenever the "Hi" message is sent
Console.WriteLine("hihihi");
});
Note: I haven't test it on my side yet as I don't have enough device. You can test it and let me know if it works.

Resources