Calling sendFollowerNotification Firebase Function from Android App - android

So I realize that, since version 12.0, you can call Firebase Functions directly from an Android app... this makes sense with the given example for sending messages:
private Task<String> addMessage(String text) {
// Create the arguments to the callable function.
Map<String, Object> data = new HashMap<>();
data.put("text", text);
data.put("push", true);
return mFunctions
.getHttpsCallable("addMessage")
.call(data)
.continueWith(new Continuation<HttpsCallableResult, String>() {
#Override
public String then(#NonNull Task<HttpsCallableResult> task) throws Exception {
// This continuation runs on either success or failure, but if the task
// has failed then getResult() will throw an Exception which will be
// propagated down.
String result = (String) task.getResult().getData();
return result;
}
});
}
...where you're sending text to a function.
exports.addMessage = functions.https.onCall((data, context) => {
// [START_EXCLUDE]
// [START readMessageData]
// Message text passed from the client.
const text = data.text;
// [END readMessageData]
// [START messageHttpsErrors]
// Checking attribute.
if (!(typeof text === 'string') || text.length === 0) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError('invalid-argument', 'The function must be called with ' +
'one arguments "text" containing the message text to add.');
}
// Checking that the user is authenticated.
if (!context.auth) {
// Throwing an HttpsError so that the client gets the error details.
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
'while authenticated.');
}
But I'm not exactly sure what I should be sending for something like the sendFollowerNotification example:
https://github.com/firebase/functions-samples/tree/master/fcm-notifications
exports.sendFollowerNotification = functions.database.ref('/followers/{followedUid}/{followerUid}')
.onWrite((change, context) => {
const followerUid = context.params.followerUid;
const followedUid = context.params.followedUid;
// If un-follow we exit the function.
if (!change.after.val()) {
return console.log('User ', followerUid, 'un-followed user', followedUid);
}
I mean... assuming the users are logged-in and have firebase UIDs and are in the database (my app automatically creates a firebase user when someone logs in)... it would appear that sendFollowerNotification just gets everything from the realtime database.
So what do I put under?:
.call(data)
And how am I retrieving the UID for the user that I'm trying to follow? For one that's logged in and using the app... I obviously already have that user's UID, token, and everything else... but I'm unsure of how to retrieve that info for the user who's about to be followed... if that makes any sense.
I've googled all over the internet and have never found an example of this particular kind of function call being used from within an android app using the new post 12.0.0 method. So I'm curious to know what the proper syntax should be.

Ok! This one really enraged me trying to figure it out... It turns out you don't need to call "sendFollowerNotification" at all.. All it does is it listens for changes to the Firebase Realtime Database. If you make changes in the syntax where sendFollowerNotification is looking... it automatically sends out the notification.
There's no call at all in "sendFollwerNotification" for writing users to the Realtime Database. I actually handle this at login:
private DatabaseReference mDatabase; //up top
mDatabase = FirebaseDatabase.getInstance().getReference(); //somewhere in "onCreate"
final String userId = mAuth.getUid();
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
mDatabase.child("users").child(userId).child("displayName").setValue(name);
mDatabase.child("users").child(userId).child("notificationTokens").child(refreshedToken).setValue(true);
mDatabase.child("users").child(userId).child("photoURL").setValue(avatar);
Then when one user follows another I just write that to the realtime database as well:
mDatabase.child("followers").child(user_Id).child(follower_id).setValue(true);
And that's it! The second a new follower is added to the RealTime Database... sendFollwerNotification will automatically send out a notification. You just need to setup a listener in your app for receiving messages and where it should redirect your users once they tap a message that's been received and you're done.

Related

Firebase Real-Time Database what would happen to concurrent requests in this case?

I have 2 nodes in my database items and my_items.
I have this function that does a multi-path update, this function deletes an item from items and adds it to the user's my_items:
I secured the my_items node in a way that you can only write to it if the item exists in items, otherwise it fails:
private void getItem(String uid, String item_key) {
Map<String, Object> updates = new HashMap<>();
//delete item
updates.put("items/" + item_key, null);
//give the item to the user
updates.put("my_items/" + uid + "/" + item_key, itemObject);
mDatabaseReference.updateChildren(updates);
}
Question (In theory):
Knowing that Firebase Database handles requests one by one.
If users A,B,C,D called the getItem(..) function for the same item id together at the same time:
Does this makes sense:
lets say A's request reached the server first and Succeeds
(now the item was deleted from items and added to user A at my_items)
B: Fails
(because the item no longer exists in items, so security rule of my_items prevents this)
C: Fails
(same reason)
D: Fails
(same reason)
Is this what would happen ? Or I got this wrong ?
Thanks.
The Firebase Realtime Database processes (write) operations sequentially. So any previous write operation will have been completed before the next one is processed. That means that any data written by a previous user will indeed be present in the root variable in security rules evaluation for later write operations.
This Firebase Blog post discusses atomic operations. RTDB acts on requests as they arrive. RTDB processes events as they arrive. There is another question that might be relevant Does Firebase always guarantee added events in order?
You can ... do this in a single atomic update to both locations:
JavaScript
var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
// Generate a new push ID for the new post
var newPostRef = ref.child("posts").push();
var newPostKey = newPostRef.key();
// Create the data we want to update
var updatedUserData = {};
updatedUserData["user/posts/" + newPostKey] = true;
updatedUserData["posts/" + newPostKey] = {
title: "New Post",
content: "Here is my new post!"
};
// Do a deep-path update
ref.update(updatedUserData, function(error) {
if (error) {
console.log("Error updating data:", error);
}
});
Java
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
// Generate a new push ID for the new post
Firebase newPostRef = ref.child("posts").push();
String newPostKey = newPostRef.getKey();
// Create the data we want to update
Map newPost = new HashMap();
newPost.put("title", "New Post");
newPost.put("content", "Here is my new post!");
Map updatedUserData = new HashMap();
updatedUserData.put("users/posts/" + newPostKey, true);
updatedUserData.put("posts/" + newPostKey, newPost);
// Do a deep-path update
ref.updateChildren(updatedUserData, new Firebase.CompletionListener() {
#Override
public void onComplete(FirebaseError firebaseError, Firebase firebase) {
if (firebaseError != null) {
System.out.println("Error updating data: " + firebaseError.getMessage());
}
}
});
Objective-C
Firebase *ref = [[Firebase alloc] initWithUrl: #"https://<YOUR-FIREBASE-APP>.firebaseio.com"];
// Generate a new push ID for the new post
Firebase *newPostRef = [[ref childByAppendingPath:#"posts"] childByAutoId];
NSString *newPostKey = newPostRef.key;
NSString *updateUserPath = [NSString stringWithFormat:#"users/posts/%#", newPostKey];
NSString *updatePostPath = [NSString stringWithFormat:#"posts/%#", newPostKey];
// Create the data we want to update
NSMutableDictionary *updatedUserData = [[NSMutableDictionary alloc] init];
[updatedUserData setValue:[NSNumber numberWithBool:YES] forKey:updateUserPath];
[updatedUserData setValue:#{#"title": #"New Post", #"content": #"Here is my new post!"} forKey:updatePostPath];
// Do a deep-path update
[ref updateChildValues:updatedUserData withCompletionBlock:^(NSError *error, Firebase *ref) {
if (error) {
NSLog(#"Error updating data: %#", error.debugDescription);
}
}];
Swift
"https://<YOUR-FIREBASE-APP>.firebaseio.com") // Generate a new push
ID for the new post let newPostRef =
ref.childByAppendingPath("posts").childByAutoId() let newPostKey =
newPostRef.key // Create the data we want to update let
updatedUserData = ["users/posts/(newPostKey)": true,
"posts/(newPostKey)": > ["title": "New Post", "content": "Here is my
new post!"]] // Do a deep-path update
ref.updateChildValues(updatedUserData, withCompletionBlock: { (error,
ref) -> > Void in
if (error) {
print("Error updating data: (error.description)")
} })
Deep path updates let you write cleaner code and easily denormalize
data across multiple nodes in your Firebase database.

Android Firestore Stripe - Add Payment Source

Update At Bottom
I am trying to build a signup page in my Android app that signs users up for a subscription through Stripe. What I am stuck on is adding a payment source from Android, through a cloud function, and receive a token from Stripe.
I currently have solved, automatically adding a newly created User to Stripe. As well creating the subscription when (/users/{userId}/membership/token) is written to, or changed.
On Android I am able to obtain the credit card data through the input..
PaymentMethodCreateParams.Card card = cardInputWidget.getPaymentMethodCard();
I next need to submit this to my cloud function by using..
mFunctions = FirebaseFunctions.getInstance();
mFunctions.getHttpsCallable("addPaymentSource")
.call()
.addOnCompleteListener(task -> {
...
Being I am having trouble finding information on this, here is all I have for this cloud function (Javascript)
exports.addPaymentSource = functions.https.onCall((data, context) =>{
const pm = await stripe.paymentMethods.attach('pm_678', {customer: 'cus_123'});
return admin.firestore().collection('users').doc(user.uid).get('membership').set({token: token});
}
I need to obtain the customer number which is saved at - /users/{user.uid}/customerId'. As well pass the payment method through my http data call, and pass/obtain the user_id (which would have been created long before all this).
I got this far watching this youtube video and converting my code over. Subscription Payments with Stripe, Angular, and Firebase
I also referenced Stripe's Cloud Function examples quite a bit. The one issue is everyone seems to be using this code (below), which doesn't work in my implementation. With most guides/examples not being used for Subscriptions.
// Add a payment source (card) for a user by writing a stripe payment source token to Cloud Firestore
exports.addPaymentSource = functions.firestore.document('/stripe_customers/{userId}/tokens/{pushId}').onCreate(async (snap, context) => {
const source = snap.data();
const token = source.token;
if (source === null){
return null;
}
try {
const snapshot = await admin.firestore().collection('stripe_customers').doc(context.params.userId).get();
const customer = snapshot.data().customer_id;
const response = await stripe.customers.createSource(customer, {source: token});
return admin.firestore().collection('stripe_customers').doc(context.params.userId).collection("sources").doc(response.fingerprint).set(response, {merge: true});
} catch (error) {
await snap.ref.set({'error':userFacingMessage(error)},{merge:true});
return reportError(error, {user: context.params.userId});
}
});
Update:
Made some small changes to get try and get this to work..
exports.addPaymentSource = functions.https.onCall((data, context) =>{
///users/{userId}/membership/token
// Create Payment Method
const paymentMethod = stripe.paymentMethods.create(
{
type: 'card',
card: {
number: '4242424242424242',
exp_month: 5,
exp_year: 2021,
cvc: '314',
},
}).then(pm => {
console.log('paymentMethod: ', paymentMethod.id);
return stripe.paymentMethods.attach(paymentMethod.id, { customer: 'cus_HCQNxmI5CSlIV5' })
.then(pm => {
return admin.firestore().collection('users').doc(user.uid).get('membership').set({token: pm.id});
});
});
});
I am getting close, the problem is paymentMethod.id is 'undefined'
While I'm not a Firebase expert, on your Android side, you want to call your cloud function with parameters of the Customer ID and PaymentMethod ID in order to pass them to your cloud function.
Passing parameters shown here: https://stackoverflow.com/a/56298213/10654456
Then in your cloud function, you want to attach the PaymentMethod to the Customer (as you are doing using stripe-node) and make it the Customer's default for Subscriptions, as shown here: https://stripe.com/docs/billing/subscriptions/payment#signup-3
Then, you should create a Subscription on the Customer for a particular Plan, again using stripe-node, as shown here https://stripe.com/docs/billing/subscriptions/payment#signup-4
Here I have my functioning code. (I used some placeholder data to fill in the variables)
exports.addPaymentSource = functions.https.onCall((data, context) =>{
// Create Payment Method
stripe.paymentMethods.create( {
type: 'card',
card: {
number: '4242424242424242',
exp_month: 5,
exp_year: 2021,
cvc: '314',
},
})
.then(pm => {
return stripe.paymentMethods.attach(pm.id, { customer: 'cus_HCCNMAAwRhNM3c' })
})
.then(pm => {
console.log('final step');
console.log('paymentMethod: ', pm.id);
admin.firestore().collection('users').doc('LzgbQBtk0QSZi7QISIbV').set({token: pm.id});
return admin.firestore().collection('users').doc(user.uid).collection('membership').set({token: pm.id});
})
.catch(error => { return null });
});
So I manually pasted in some variables to confirm my features were functioning. The CustomerID and card details need to be passed in from the Android app. These card details are the only ones I should need for a subscription
'pm' is the returned Payment Method object, in which id is the variable that needs to be attached to the user.
Finally pm.id is the token that must be saved inside into the firestore. Doing this triggers my subscription setup cloud function(not displayed).
The code displayed shows how to avoid nested then statements, and Android firestore direct function calling. While also not shown, the data field can call upon any variable's key word "data.cardnbr".
The method avoids any use of SetupIntents. While this is incredibly effective for Subscription based charging, it might not be best practice for direct charges.

Adding data to FirebaseDatabase when there is no Internet connection

If I try to send data to FirebaseDatabase in the absence of an Internet connection, then, as expected, nothing happens. If you turn on the Internet, then this data is added themselves, and even if I restarted the application. And they are added to the very end of the database. Even if you add data after this, the offline post will still be the last.
I think to solve the problem by checking the Internet before sending. Maybe there are any other solution methods?
I send data in a simple way:
final String phone_val = etPhone.getText().toString().trim();
final String comment_val = etComment.getText().toString().trim();
DatabaseReference newTrip = mDatabase.push();
newTrip.child("phone").setValue(phone_val);
newTrip.child("comment").setValue(comment_val);
newTrip.child("type").setValue(1);
startActivity(new Intent(AddDriverActivity.this, TripActivity.class));
finishAffinity();
Firstly, if you haven't already, read through the offline capabilities documentation so you have a general grasp of how Firebase behaves while offline.
Next, we'll clean up your write operations so that they are a single atomic operation rather than a few separate write operations.
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference newTrip = mDatabase.push();
newTrip.setValue(tripData);
As stated in the offline capabilities documentation, you can check whether your app is offline by checking the special database location /.info/connected which returns the current connection state. This value will be either true or false.
While you could check this value before posting your trips, the connection state may change while you are sending the data.
Even if you add data after this, the offline post will still be the last.
This is the trickier part to manage. I think the easiest way to deal with this is to have a "staging" section of your database and then move data as it is created at this location to the main storage location using a Cloud Function for Firebase.
Client Side
Let's say you are storing these trips in /trips/someTripId.
private DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
private DatabaseReference mAllTripsRef = mDatabase.child('trips');
To add a new trip, you would use:
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference mNewTripRef = mAllTripsRef.push();
mNewTripRef.setValue(tripData);
Because references created by push() are based on the estimated time of the Firebase servers, they will be ordered by when they were created rather than when they are received by the Firebase servers. But if you wanted to preserve that offline trips are always last, instead of writing new trips to /trips, you would instead write to /trips-staging.
private DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
// trips that have been posted to "online" database
private DatabaseReference mAllTripsRef = mDatabase.child('trips');
// trips yet to be posted online
private DatabaseReference mStagingTripsRef = mDatabase.child('trips-staging');
New data would be added using:
HashMap<String, Object> tripData = new HashMap<>();
tripData.put("phone", phone_val);
tripData.put("comment", comment_val);
tripData.put("type", 1);
DatabaseReference mNewTripRef = stagingTripsRef.push();
mNewTripRef.setValue(tripData);
Now that we have the reference to the trip waiting to be posted, mNewTripRef, we can add a listener to it to see when it has been posted.
In the cloud side below, we are going to make it so that if there is data at /trips-staging/someTripId and it is just a string, then the trip has been received and posted by the server to the location /trips/<string-value>.
ValueEventListener stagingTripListener = new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get trip data
Object tripData = dataSnapshot.getValue();
if (tripData == null) {
// Data has been deleted!
// Disconnect this listener
mNewTripRef.removeEventListener(this);
// TODO: What now?
} else if (tripData instanceof String) {
// Data has been moved!
DatabaseReference postedTripRef = mAllTripsRef.child((String) tripData);
// Disconnect this listener
mNewTripRef.removeEventListener(this);
Log.i(TAG, "stagingTripListener:onDataChange", "New trip has been successfully posted as trip '" + mNewTripRef.getKey() + "'");
// TODO: do something with postedTripRef
} else {
// ignore - the trip hasn't been moved yet, continue waiting
// tripData is a Map<string, Object> with our original data
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
// Getting this trip failed, log a message
Log.w(TAG, "stagingTripListener:onCancelled", databaseError.toException());
}
};
mNewTripRef.addValueEventListener(stagingTripListener);
Cloud Side
Now, we need to move these new trips over to /trips once they are received on the server. For this we can use a Cloud Function for Firebase that listens to Realtime Database events. For our use case, we want to exclusively listen to data creation events that happen on our /trips-staging location.
When the Cloud Function is triggered, it should take the data at /trips-staging/someId and move it to /trips/someNewId. It is probably also a good idea to store where we moved the data to at the old location if it is ever needed but also so we can tell when the trip has been received by the server.
After following the Getting Started documentation up to Step 4, you can use the following code as your index.js or index.ts file and then deploy it using firebase deploy --only functions.
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(); // use defaults
export const moveTripsFromStaging = functions.database.ref('/trips-staging/{stagingTripId}')
.onCreate((snapshot, context) => {
const stagingTripId = snapshot.key;
const tripData = snapshot.val();
// get reference to the root of the database
let dbRootRef = admin.database().ref();
let allTripsRef = dbRootRef.child('trips');
let newTripRef = allTripsRef.push();
return dbRootRef.update({
// move data to it's new home
['trips/' + newTripRef.key]: tripData,
// save where we moved data to as a simple string containing the new key in /trips
['trips-staging/' + stagingTripId]: newTripRef.key // or set to null to delete it
});
})
Once deployed, you should see new trips that are uploaded to /trips-staging be received by the server and then moved across to /trips in the order that server receives them.

Firebase Auth Custom claims not propagating to client

I have a user with UID 1 where the custom claims are set as,
frompos=true
I am setting new custom claims to this user from the ADMIN SDK for java the following way:
Map<String,Object> claims = new HashMap<>();
claims.put("frompos",false);
FirebaseAuth.getInstance().setCustomUserClaimsAsync("1", claims).get(10000,
TimeUnit.MILLISECONDS);
I print the claims on the server side to check if the claims are set:
UserRecord user = FirebaseAuth.getInstance().getUserAsync("1").get(10000,
TimeUnit.MILLISECONDS);
LOG.debug("user new claims " + user.getCustomClaims());
The result as expected is that the claims get set:
user new claims {frompos=false}
Now on the android sdk side, I have the user already logged in so I am refreshing the ID token manually to propagate the claims as the docs say
(https://firebase.google.com/docs/auth/admin/custom-claims)
FirebaseAuth.getInstance().getCurrentUser().getIdToken(true).addOnCompleteListener(new OnCompleteListener<GetTokenResult>() {
#Override
public void onComplete(#NonNull Task<GetTokenResult> task) {
if(task.isSuccessful()){
Log.d("FragmentCreate","Success refreshing token "+(FirebaseAuth.getInstance().getCurrentUser()==null));
Log.d("FragmentCreate","New token "+task.getResult().getToken());
}
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.d("FragmentCreate","Failure refreshing token "+(FirebaseAuth.getInstance().getCurrentUser()==null)+" "+e.toString());
}
});
Now I use the printed Id Token printed here and verify it on server side and print the claims from it
FirebaseToken tokenTest = FirebaseAuth.getInstance(ahmedabadRepoApp).verifyIdTokenAsync(token).get(10000,TimeUnit.MILLISECONDS);
LOG.debug("Token claims are "+tokenTest.getClaims());
But the claims printed here are:
{"aud":"ahmedabadrepo","auth_time":1514724115,"email_verified":false,"exp":1514730425,"iat":1514726825,"iss":"https://securetoken.google.com/ahmedabadrepo","sub":"1","frompos":true,"user_id":"1","firebase":{"identities":{},"sign_in_provider":"custom"}}
Thus the frompos value did not propagate to the client sdk even though I did refresh the Id token manually.
I was having the same issue in angular - I set the claim using the Admin SDK on the server, but then they would not be in the user on the client.
Using the following I can see the claims in the payload:
this.firebaseAuth.auth.currentUser.getIdToken().then(idToken => {
const payload = JSON.parse(this.b64DecodeUnicode(idToken.split('.')[1]))
console.log(payload);
}
)
b64DecodeUnicode(str) {
return decodeURIComponent(atob(str).replace(/(.)/g, function (m, p) {
var code = p.charCodeAt(0).toString(16).toUpperCase();
if (code.length < 2) {
code = '0' + code;
}
return '%' + code;
}));
}
Here is a good write up of this where I copied the above:
At the moment the client-side code must parse and decode the user’s ID
token to extract the claims embedded within. In the future, the
Firebase client SDKs are likely to provide a simpler API for this use
case.
Relevant info from Firebase Docs:
Custom claims can only be retrieved through the user's ID token.
Access to these claims may be necessary to modify the client UI based
on the user's role or access level. However, backend access should
always be enforced through the ID token after validating it and
parsing its claims. Custom claims should not be sent directly to the
backend, as they can't be trusted outside of the token.
Once the latest claims have propagated to a user's ID token, you can
get these claims by retrieving the ID token first and then parsing its
payload (base64 decoded):
// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
firebase.auth().currentUser.getIdToken()
.then((idToken) => {
// Parse the ID token.
const payload = JSON.parse(b64DecodeUnicode(idToken.split('.')[1]));
// Confirm the user is an Admin.
if (!!payload['admin']) {
showAdminUI();
}
})
.catch((error) => {
console.log(error);
This might help: https://stackoverflow.com/a/38284384/9797228
firebase.auth().currentUser.getIdToken(true)
The client sdk is caching the old token (old claims).
You should add a mechanism to refresh it after changing the claims (eg. push notification) or just wait for the old token to expires or user to lougout and login again.
It's explained here https://youtu.be/3hj_r_N0qMs?t=719
Edit
You can force the sdk to refresh the token using firebase.auth().currentUser.getIdToken(true)

Parse Cloud code: How to modify authData column on a User?

I have been trying to implement 'Login via Facebook' functionality in my application. I'm using Parse as backend. The problem arises due to the fact that Parse creates a new entry in the User Table with authData and random username, even if there's already a user exists with the same email in the User Table. So what I'm doing for now is ask user to connect Facebook via Facebook SDK, and get his email. Then I call this cloud code from Android that will search the user associated with that email in the Parse User Table. If a user exists, I'm trying to update the authData field. It can update any column but authData. It gives the following error while updating authData from cloud code.
Uncaught Tried to save an object with a pointer to a new, unsaved object
Following is the cloud code I'm using:
Parse.Cloud.define("facebookLogin", function(request, response){
Parse.Cloud.useMasterKey();
var email = request.params.email;
var userQuery = new Parse.Query(Parse.User);
userQuery.equalTo("email", email);
userQuery.first({
success: function(user) {
if (user) {
Parse.Cloud.useMasterKey();
var authData = util.createFBAuthData(request.params.id, request.params.access_token, request.params.expiration_date);
console.log(authData);
user.set("authData", authData);
user.save(null, {
success: function(rest) {
response.success(rest);
},
error: function(error) {
console.log(error);
response.error(messageLog.getErrorMessage());
}
});
} else {
createNewUserEntry(request);
}
},
error: function() {
response.error(messageLog.getErrorMessage());
}
});
})
I don't understand why Parse isn't allowing to edit/update authData using cloud code. I'm able to manually insert (Copy/Paste) the json into the authData field in the User Table from the Parse Dashboard. Anybody who has faced this issue?

Categories

Resources