I have created a xamarin forms application and one of the requirements is as follows: The user should be able to press a button and create a geofence. When the geofence is created, the application constantly checks if the user is in a geofence. It that's true, the mobile app should show a notification sayinf: you have entered geofence.
I use Shiny to implement the feature based on this tutorial: "https://allancritchie.net/posts/shiny-geofencing".
But the problem is that it isn't doing anything. I have set the geofence 50 meters from my house, and the radius for checking to 200 meter, so when I build the app, a couple of seconds/minuts I should receive a nofitication right? Or do I need to go out of the fence first and then in? How can I fix this issue...
my code:
// shiny doesn't usually manage your viewmodels, so we'll do this for now
var geofences = ShinyHost.Resolve<IGeofenceManager>();
var notifications = ShinyHost.Resolve<INotificationManager>();
Register = new Command(async () =>
{
// this is really only required on iOS, but do it to be safe
var access = await notifications.RequestAccess();
if (access == AccessState.Available)
{
await geofences.StartMonitoring(new GeofenceRegion(
"CN Tower - Toronto, Canada",
new Position(52.079779, 4.337133),
Distance.FromMeters(200)
)
{
NotifyOnEntry = true,
NotifyOnExit = true,
SingleUse = false
});
}
});
public class GeofenceDelegate : IGeofenceDelegate
{
private readonly INotificationManager _notifications;
public GeofenceDelegate(INotificationManager notifications)
{
_notifications = notifications;
}
public async Task OnStatusChanged(GeofenceState newStatus, GeofenceRegion region)
{
if (newStatus == GeofenceState.Entered)
{
await GeofenceEntered(region);
}
else if (newStatus == GeofenceState.Exited)
{
await GeofenceLeft(region);
}
}
}
I have exactly the same code as the tutorial, and the geofence is created when I click a button if I debug it. So everything looks fine to me..
I had a similar issue and just assumed that when you start inside a fence it won't recognize you as entering/or exiting since you are already inside it.
I had to walk out/in of the created fence to trigger the 'OnStatusChanged' method.
Important to note is, that it wouldn't enter the method unless my app activated the phones GPS-system (e.g. re-requesting the current position periodically), but im not sure if that is caused by my old ass phone (running Android6.1) or if it is mend to work that way.
Also note, that i didn't use the shiny.notifications and just showed a popup when the method was run (for testing purposes) like this:
public async Task OnStatusChanged(GeofenceState newStatus, GeofenceRegion region)
{
await Application.Current.MainPage.DisplayAlert(newStatus.ToString(), $"{region.Identifier}", "Ok");
}
Hope this helps ;)
Related
I do the tutorial from CodeLab his project with all steps are here Github. Codelab helped me lot, Thanks!
I do the steps and deleted all IAP products from tutorial and added only subscribable products. i have two purchase product "Normal" and "Ultimate" in the same Family in Appstoreconnect. Its working well, but I found a problem:
Situation A:
When the user subscribed one from them its working all fine, but when the user want to subscribe the other like from "Normal" to "Ultimate" or "Ultimate" to "Normal" in his Validate activ time, then Firebase Cloud don't update his Purchase (Produkt ID and Order ID Is still the old ID's). When Firebase don't update, then get the user not his upgrade to other subscribe instantly. He get there upgrade to the other purchase after a year.
Situation B:
The same Problem, but Outside from the App.
User subscribed one product, then he go outside from app in his Appstore settings and change his subscribe product. Firebase get a info from Apple, but Firebase Cloud don't update the subscription information from User.
can u or have u solve this problem?
my changes from Codelab ->
Constant.dart
const cloudRegion = 'europe-west1';
const subscriptionList = ["kunde_1_fahrzeug", "kunde_3_fahrzeug"];
//storeKeySubscription
const subscription_kunde_1_fahrzeug = 'kunde_1_fahrzeug';
const subscription_kunde_3_fahrzeug = 'kunde_3_fahrzeug';
IAPRepo.dart
void updatePurchases() {
// omitted
// hasActiveSubscription = purchases.any((element) => element.productId == subscription_kunde_1_fahrzeug && element.status != Status.expired);
//hasActiveSubscription = purchases.any((element) => element.productId == subscriptionList && element.status != Status.expired);
hasActiveSubscription = purchases.any((element) => subscriptionList.any((x) => x == element.productId) && element.status != Status.expired);
for(PastPurchase x in purchases){
print("Gelb hasActiveSubscription IAP-REPO : ${x.productId} - ${x.status}");
};
hasUpgrade = purchases.any(
(element) => subscriptionList.any((x) => x == element.productId),
);
/*
hasUpgrade = purchases.any(
(element) => element.productId == storeKeyUpgrade,
);
*/
notifyListeners();
// omitted
}
XXX_Purchase
void purchasesUpdate() {
// omitted
if (products.isNotEmpty) {
// subscriptions = products .where((element) => element.productDetails.id == subscription_kunde_1_fahrzeug) .toList();
subscriptions = products
.where((element) => subscriptionList.any((x) => x == element.productDetails.id))
.toList();
upgrades = products
.where((element) => subscriptionList.any((x) => x == element.productDetails.id))
.toList();
}
// omitted
}
Future<void> loadPurchases() async {
// omitted
const ids = <String>{
subscription_kunde_1_fahrzeug,
subscription_kunde_3_fahrzeug,
//storeKeyUpgrade,
};
// omitted
}
Future<void> buy(PurchasableProduct product) async {
// omitted
// case storeKeyConsumable:
// await iapConnection.buyConsumable(purchaseParam: purchaseParam);
// break;
case subscription_kunde_1_fahrzeug:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
break;
case subscription_kunde_3_fahrzeug:
//case storeKeyUpgrade:
await iapConnection.buyNonConsumable(purchaseParam: purchaseParam);
break;
// omitted
}
Future<void> _handlePurchase(PurchaseDetails purchaseDetails) async {
// omitted
if (validPurchase) {
// Apply changes locally
switch (purchaseDetails.productID) {
case subscription_kunde_1_fahrzeug:
print("Orange: ID Produkt: ${purchaseDetails.productID}, ${purchaseDetails.transactionDate}, ${purchaseDetails.verificationData}, ${purchaseDetails.status}, ${purchaseDetails.purchaseID}, ${purchaseDetails.pendingCompletePurchase}, switch (purchaseDetails.productID) case: subscription_kunde_1_fahrzeug");
counter.applyPaidMultiplier_kunde_1_fahrzeug();
break;
case subscription_kunde_3_fahrzeug:
print("Orange: ID Produkt: ${purchaseDetails.productID}, ${purchaseDetails.transactionDate}, ${purchaseDetails.verificationData}, ${purchaseDetails.status}, ${purchaseDetails.purchaseID}, ${purchaseDetails.pendingCompletePurchase}, switch (purchaseDetails.productID) case: subscription_kunde_3_fahrzeug");
counter.applyPaidMultiplier_kunde_3_fahrzeug();
break;
// case storeKeyConsumable:
// counter.addBoughtDashes(2000);
// break;
/* case storeKeyUpgrade:
_beautifiedDashUpgrade = true;
break;
*/
// omitted
}
PastPurchase
#immutable
class PastPurchase {
// omitted
String get title {
switch (productId) {
case subscription_kunde_1_fahrzeug:
return 'Subscription';
case subscription_kunde_3_fahrzeug:
return 'Subscription';
default:
return productId;
}
}
// omitted
}
Firebase Backend
export interface ProductData {
productId: string;
type: "SUBSCRIPTION" | "NON_SUBSCRIPTION";
}
export const productDataMap: { [productId: string]: ProductData } = {
"kunde_1_fahrzeug": {
productId: "kunde_1_fahrzeug",
type: "SUBSCRIPTION",
},
"kunde_3_fahrzeug": {
productId: "kunde_3_fahrzeug",
type: "SUBSCRIPTION",
},
};
The problem is the codelab is unrealistically simplistic related to subscriptions, and relies on a node.js package that also doesn't handle in-family subscription changes. Subscription changes with apple don't provide the new product_id, they provide the new subscription id as auto_renew_product_id, and the product_id stays the same from the original transaction. Turn verbose: true to see it when running your function.
So, to fix, you'd need a third function for in-app subscription changes, which you can't validate and return properly from apple-receipt-verify because that package doesn't provide the auto_renew_product_id that you're switching to. So you'll need a new way to validate the receipt.
For changes outside the app, you'll need to fix the handleServerEvent, because that doesn't work to change subscriptions from outside app.
I suggest RevenueCat. The cost is minimal, and you'll have a company with a vested interest in updating the API's.
Your revenue foundation needs to be firm, and while the Google Play Store performance of the package and server-side code was spotless for me...their interaction with Apple is barely functional for the simplest of scenarios.
Edit: A couple points to clarify, expound on for future readers.
There is a typo as of May 2022 in the code lab. You need to use Type 1 Notifications from Apple.
Apple handles in-group subscription changes itself. It will unsubscribe and subscribe within a group. You also need to rank your in-group subscriptions with apple. Android just changes right away.
Apple handles down-grade, cross-grade and upgrade differently. All downgrades must finish their term before changing. This prevents apple from having to refund. So when you downgrade, the term must finish, then apple will change the sub and fire the server event.
The code lab logic has Android creating/canceling subs and leaving a sub history trail of purchases documents in firestore...this is because every sub change is a cancel and create. Apple however changes the subs internally, and then fires to the server telling you what changed. So the code lab does not have a doc trail detailing any subscription history..each sub is one doc always...just changes product id and status.
In short: doing the codelab, and getting to a functional level for your app will definitely give you a good understanding of how each store handles subs...and how differently they handle them. As it relates to the in_app_purchase plug-in, server side validation and performance...while elegantly done, it was the bare minimum guidance given by the codelab and package.
I'm developing a web app using Flutter Web and Firebase.
I have to handle the Firebase Login.
Let's assume we have two screens, the situation that I want to achieve is the following:
if the user is not logged in:
Redirect the user on the login page
If the user is not logged in:
if he lands on the login page, redirect him on the homepage
I've implemented a functions that checks the current user in firebase and acts as following:
void checkAuthentication() {
var url = window.location.href;
var navigationService = locator<NavigationService>();
var loggedIn = this.isUserLoggedIn();
if (!loggedIn) {
navigationService.replaceWith(Routes.login);
} else {
if (url.contains("Login")) {
navigationService.replaceWith(Routes.homepage);
}
}
}
the navigationService is a service I took from the stacked package (https://pub.dev/packages/stacked).
This solution works, but has two problems:
this is not the right approach to do this. It's not possible that I have to call this in each screen page
When you are redirected you can see a transition with the new page presented.
My question:
How would you manage this in Flutter Web in a unique point in the code?
Is there a better way to achieve this differnt from the one I shown here?
If you have the time checkout the first couple videos from The Net Ninja's Flutter/Firebase tutorial which go over basic authentication. I am pretty new to Flutter so I'll try my best to explain how he does it without going into detail:
He has a file wrapper.dart which uses a custom User object which constantly streams the authentication state. The wrapper decides where the user should navigate depending on authentication state.
Widget build(BuildContext context) {
final user = Provider.of<User>(context);
print(user);
// return either home or authenticate
if (user == null) {
return Authenticate();
} else {
return Home();
}
}
I am using this code in my project and it works well.
Authenticate with Firebase with a Phone Number (JS) requires a mandatory reCAPTCHA verifier, it takes the ID of the container. For the ID of the container, I am generating a random one -
firebase_recaptcha_container: "recaptcha-container",
firebase_recaptcha_reset: function() {
if (typeof appVerifier != "undefined") {
appVerifier.reset()
appVerifier.clear()
}
let id = loadJS.firebase_recaptcha_container
let newID = loadJS.randomString(10)
$("#"+id).contents().remove()
$("#"+id).prop("id", newID)
loadJS.firebase_recaptcha_container = newID
return newID
}
then requesting for the RecaptchaVerifier and upon receiving I set this as a global variable window.appVerifier .
firebase_recaptcha: function(name_r="default") {
let promiseD = new firebase.auth.RecaptchaVerifier(name_r, {
'size': 'invisible',
'callback': function(response) {
resolve(response)
},
'expired-callback': function(r) {
console.log("expired", r)
},
'isolated' : false
});
return promiseD
},
_____________________
let container_recaptcha = $utils.firebase_recaptcha_reset()
window.appVerifier = await $utils.firebase_recaptcha(container_recaptcha)
It works totally fine for the very first time. But its a honest mistake for users not to use correct phone number. So for next time, I am doing the same thing again and getting error while generating the RecaptchaVerifier -
reCAPTCHA has already been rendered in this element
Which sadly does not make sense as the new element is totally different and also clear, reset methods were called following the documentation. I am neither using any other reCaptcha on this page. Refreshing the page might be a possible solution but that I really hate. Any insight would be helpful.
Thanks!
Finally found the solution, looks like it was a stupid mistake!
Invoking firebase.auth.RecaptchaVerifier adds new recaptcha scripts, every time! Hence all needed to be done is, calling it once, it does the rest on its own.
This won't get fixed just by implementing recaptchaVerifier.clear() method.
In the callback where you are passing this appVerifier, you'll have to implement the above clear method and add that "recaptcha-container" using ref
The below would be the element in render method:
<div ref={recaptchaWrapperRef}>
<div id="recaptcha-container"></div>
</div>
GenerateCaptcha function:
const generateRecaptcha = () => {
appVerifier = new RecaptchaVerifier(
"recaptcha-container",
{
size: "invisible",
},
authentication
);
Inside submit Callback:
if (appVerifier && recaptchaWrapperRef.current) {
appVerifier.clear();
recaptchaWrapperRef.current.innerHTML = `<div id="recaptcha-container"></div>`;
}
// Initialize new reCaptcha verifier
generateRecaptcha();
Using GoogleAppMaker how to create a data source from google contacts. There is an employee HR example app but I want to similarly manage contacts (add, modify, delete) and use select criteria.
At this time this task is not trivial in App Maker and it is pretty much generic. We can change question wording to CRUD operations with 3rd party datasources. Let's break it into smaller parts and address them separately.
Read/list contacts
This task is relatively easy. You need to use Calculated Model to proxy Apps Scripts Contacts API response. Once you create model with subset of fields from the Contact response you can create datasource for the model and bind it to List or Table widget. You can also try to find some inspiration in Calculated Model Sample.
// Server side script
function getContacts_() {
var contacts = ContactsApp.getContacts();
var records = contacts.map(function(contact) {
var record = app.models.Contact.newRecord();
record.FirstName = contact.getGivenName();
record.LastName = contact.getFamilyName();
var companies = contact.getCompanies();
if (companies.length > 0) {
var company = companies[0];
record.Organization = company.getCompanyName();
record.Title = company.getJobTitle();
}
var emails = contact.getEmails();
if (emails.length > 0) {
record.Email = emails[0].getAddress();
}
var phones = contact.getPhones();
if (phones.length > 0) {
record.Phone = phones[0].getPhoneNumber();
}
return record;
});
return records;
}
Create/Update/Delete
Since Calculated Models have some limitations, we need to turn on our imagination to create, update and delete records from their datasources. The basic strategy will be calling server side scripts for CUD operations in response to user actions on client side. To get user's input from UI we will need to utilize page's Custom Properties, one property for each Contact field:
Here are some snippets that should explain the idea
Create
// Client script
function onSubmitContactClick(submitButton) {
var props = submitButton.root.properties;
var contact = {
FirstName: props.FirstName,
LastName: props.LastName,
Organization: props.Organization,
...
};
google.script.run
.withSuccessHandler(function() {
// Most likely we'll need to navigate user back to the
// page with contacts list and reload its datasource
// to reflect recent changes, because our `CUD` operations
// are fully detached from the list datasource
app.showPage(app.pages.Contacts);
app.datasources.Contacts.load();
})
.withFailureHandler(function() {
// TODO: Handle error
})
.createContact(contact);
}
// Server script
function createContact(contactDraft) {
var contact = ContactsApp.createContact(contactDraft.FirsName,
contactDraft.LastName,
contactDraft.Email);
contact.addCompany(contactDraft.Organization, contactDraft.Title);
contact.addPhone(ContactsApp.Field.WORK_PHONE, contactDraft.Phone);
}
Update
Idea to update contact records will be very similar to the new contact creation flow, so I skip it for now.
Delete
Assuming that delete button is located inside contacts table row.
// Client script
function onDeleteContactClick(deleteButton) {
var email = deleteButton.datasource.item.Email;
google.script.run
.withSuccessHandler(function() {
// To update contacts list we can either reload the entire
// datasource or explicitly remove deleted item on the client.
// Second option will work way faster.
var contactIndex = deleteButton.parent.childIndex;
app.datasources.Contacts.items.splice(contactIndex, 1);
})
.withFailureHandler(function() {
// TODO: Handle error
})
.deleteContact(contact);
}
// Server script
function deleteContact(email) {
var contact = ContactsApp.getContact(email);
ContactsApp.deleteContact(contact);
}
Still learning about app maker and found this presentation at Google I/O '17 "Build Powerful Custom Apps Fast with App Maker on G Suite"
At timestamp 15.24 sec some code is shown on the screen showing how to send an email to yourself once someone creates a new item can.
https://youtu.be/Q84HQgI3Dd8?t=15m27s
Question
Can anyone advise where and how this code can be implemented its pretty cool and would be a great feature to add when a record is created
Thanks in advance and no worries if you cant help
You are looking for model events:
https://developers.google.com/appmaker/models/events
In App Maker models typically have onCreate, onSave, onLoad, onDelete events. It is the best place to handle email notifications. Here is a link to App Script email API:
https://developers.google.com/apps-script/reference/mail/mail-app
I strongly recommend you to go to the Codelab for App Maker. The section Building a form to send an email describes the whole process.
The steps to highlight are:
Step 11 - Set the onClick property of the button as a custom action with the code:
var widgets = widget.parent.descendants;
var to = widgets.To.value;
var subject = widgets.Subject.value;
var msg = widgets.Msg.value;
widgets.EmailStatus.text = 'Sending email...';
SendEmail(to, subject, msg)
Step 13 - Add the following ClientScript code:
function clearEmailForm(){
var formWidgets = app.pages.Basic.descendants;
formWidgets.EmailStatus.text = "";
formWidgets.Msg.value = "";
formWidgets.To.value = "";
formWidgets.Subject.value = "";
}
function SendEmail(To, Subject, Msg){
var status = app.pages.Basic.descendants.EmailStatus;
google.script.run.withSuccessHandler(function(result) {
status.text = 'Email sent...';
clearEmailForm();
})
.SendEmail(To, Subject, Msg);
}
Step 14 - Now add the corresponding code to the ServerScript.
function SendEmail(to, subject, msg){
MailApp.sendEmail(to, subject , msg);
}