is it possible not loop the latest_receipt_info to save transaction record in app purchase - in-app-purchase

Now I am verify in app purchase transaction record, now I am fetch the latest records from app server, should I foreach the records and select database to find if it exists? There has many invalid query, any better way to get the records? Now I am doing like this:
for (Object item : latestReceiptInfo) {
if (item instanceof JSONObject) {
JSONObject jsonObjectItem = (JSONObject) item;
PayTransactionRecord record = payTransactionRecordService.getRecordByTransactionId(jsonObjectItem.getString("transaction_id"));
if (record == null) {
mapFieldAndSave(jsonObjectItem);
}
}
}
I did not want to query database in a loop. any better way?

database operate in a loop was not a good practice,first you can use transaction id to match the server latest receipt info. If failed, you should try to save the latest record into database(sorted by expre date ms).

Related

Swift - get field from randomly created document ID (Firestore)

in my application the user can sign up and by doing that I also save the firstname, lastname and uid.
This is how Firestore-Database looks like:
I would like to access the users lastname but I do not know how, as the Document-ID gets created randomly in the sign-up process.
Auth.auth().createUser(withEmail: email, password: password) { (result, err) in
//check for errors
if let err = err {
self.view.endEditing(true)
self.showError(err.localizedDescription)
}else {
//user was created successfully; store first and last name
let db = Firestore.firestore()
db.collection("users").addDocument(data: ["firstname":firstName, "lastname": lastName, "uid": result!.user.uid]) { (error) in
if error != nil {
self.showError("Error saving user data")
}
}
//transition to home
self.transitionToHome()
}
You need to know one of two things in order to get a document in Firestore:
The full path of the document, including the name of the collection and ID of the document (and any nested subcollections and document IDs)
Something about the data in that document, for example, a value of one of its fields.
If you don't know either of these things, then all you can do is fetch all the documents in a collection and try to figure out what you want by looking at everything in the collection.
The bottom line is that if you don't know which document belongs with which user, you are kind of stuck. You should store the ID of the user in the document, or as the ID of the document itself, so that you can use it to find that document later.
I suggest using the Firebase Auth user ID as the ID of the document. This is extremely common. Don't use addDocument to write it with a random ID, use setData with the ID you know.
Firestore cannot just know which document you want at any time, unless you provide a way for it to locate the document.

How do I fetch any info about user that modified ckrecord?

All I do is:
if let id = record.lastModifiedUserRecordID {
publicDatabase.fetch(withRecordID: id) { record, error in
print(record)
}
}
This is result of output:
public record: Optional(
{
creatorUserRecordID ->
lastModifiedUserRecordID ->
creationDate -> 2018-09-09 06:23:10 +0000
modificationDate -> 2018-09-09 06:23:10 +0000
})
How to retrieve any email, first name, phone number, whatever.
I'm fairly certain that this information isn't available out-of-the-box.
You would have to create your own lastUpdatedByUser field on your CKRecord and save that information with your app when a user makes a change.
You might be able to make lastUpdatedByUser a CKReference to a User record type so that you get all the user object's details.
It's possible, but a little indirect:
get the user's recordID from record.lastModifiedUserRecordID
get the record's share reference from record.share
fetch the share's record
Determine which of the share's participants has that recordID
Extract the emailAddress/phoneNumber from the participant's userIdentity.lookupInfo

Is is normal to end up with a LOT of changes on CKFetchRecordChangesOperation?

I use CloudKit in my app, which seems to be working well. However when I initialise the app on a new device, using
CKFetchRecordChangesOperation *fetchRecordChangesOperation = [[CKFetchRecordChangesOperation alloc] initWithRecordZoneID:zoneID previousServerChangeToken:NIL];
I get a LOT of changes, as all previous deletions and changes appear to be synched across.
Is there a better way? for example to just download a full set of current data and set the serverChangeToken to the current value.
When looking at the documentation of the CKServerChangeToken it looks like you can only get it when using the CKFetchRecordChangesOperation. See: https://developer.apple.com/library/prerelease/ios/documentation/CloudKit/Reference/CKServerChangeToken_class/index.html
So now how could you get a hold on that token:
Usually you will do something like this: As you can see in the CKFetchRecordChangesOperation you can initialize it with a previousServerChangeToken. This token works like a timestamp. When the operation completes, you get this token back in the fetchRecordChangesCompletionBlock. You have to save that token in for instance the user defaults. Then the next time you start a CKFetchRecordChangesOperation, you can use that token to start reading the changes since the last time you called it.
Actually saving the token can be a little tricky. I can suggest adding a property like this:
private var previousChangeToken: CKServerChangeToken? {
get {
let encodedObjectData = NSUserDefaults.standardUserDefaults().objectForKey("\(container.containerIdentifier)_lastFetchNotificationId") as? NSData
var decodedData: CKServerChangeToken? = nil
if encodedObjectData != nil {
decodedData = NSKeyedUnarchiver.unarchiveObjectWithData(encodedObjectData!) as? CKServerChangeToken
}
return decodedData
}
set(newToken) {
if newToken != nil {
NSUserDefaults.standardUserDefaults().setObject(NSKeyedArchiver.archivedDataWithRootObject(newToken!), forKey:"\(container.containerIdentifier)_lastFetchNotificationId")
}
}
}
In your case you want a new application to start with a change token that could only have been created by an other running app. So besides saving the change token to the NSUserDefaults, you should also save it in CloudKit in the public Database in one specific settings recordType record. A newly installed app that does not have a token in it's NSUserDefaults can then read the token from your CloudKit settings record.

Stripe - check if a customer exists

In the stripe documentation they have this:
customer = Stripe::Customer.create(email: params[:stripeEmail], card: params[:stripeToken])
charge = Stripe::Charge.create(customer: customer.id, amount: price, description: '123', currency: 'usd')
But I think it's wrong as for each payment we have to check if a customer exists first, I just tried it with a test account and it turned out there were created a number of different customers with the same email but different ids.
How do I check if a customer already exists?
There is no check on Stripe's end to ensure uniqueness of customers (email, card, name, etc,) and this is something you have to do on your end.
Usually, when you create a customer with a specific email address you need to associate the customer id you got back from the API with the email address. Then next time, you check whether this email address is already in your system and either create a new customer or re-use the customer id from the previous time.
You may check if the customer exists or not by calling GET /customers with the email as a form-urlencoded parameter. You will get 200 OK response but the returned data[] will be empty if this customer email is not there.
https://stripe.com/docs/api/customers/list
I think a simple "exists" test is essential to avoid unwanted exceptions being generated.
If your customer is new then they won't have a stripe ID, which is fine. However you might get a situation where you are using an existing customer with an existing stripe ID, but have performed some sort of dormant clean-up Stripe side. In which case its good practice to test the supplied stripeID to see if its still valid.
For example I store the stripeID with the customer profile. When I make a payment I check to see if the stripeID field in the profile exists. If it does I fire off a API request to my backend to check its existence.
Something like this (.net backend example).
[HttpGet]
public ActionResult<bool> CheckCustomer(string id)
{
var service = new CustomerService();
Customer cust;
try
{
service.Get(id);
}
catch
{
return Ok(false);
}
if (cust.Deleted ?? false)
{
return Ok(false);
}
return Ok(true);
}
I don't need the result, just it not throwing an exception is good enough. However, when you delete customers in Stripe live it keeps a copy of the ID stub and sets the delete property to true, so you need to check for that too.
The front end then can either use the stripeId for the payment or create a new stripe customer first then take the payment.
You can search for the customer by email address...
public async Task<Customer> GetCustomerByEmail(string oneEmail)
{
var service = new CustomerService();
try
{
StripeList<Customer> customerList = await service.ListAsync(new CustomerListOptions() { Email=oneEmail },null);
Debug.WriteLine(customerList.ToList().Count);
return customerList.ToList().FirstOrDefault();
}
catch
{
return null;
}
}

Cloud code - "Execution timed out" during saveAll call in afterSave

Situation
I have a messaging Android app providing following feature(s)
to send message direct message to one selected recipient
to create public announcement that all users using the app receive (except author)
each user sees on his phone a list of messages he got
each message is either unread, read, or deleted
I use Parse.com as back-end
Current implementation
On Android client
When there is a new message, a new messageRequest of the MessageRequest class is created
If the message should be direct, the messageRequest has type 0, otherwise type 1
If the message is direct, there is recipient stored in the messageRequest object
The messageRequest object is stored to parse.com
On parse.com back-end
In the afterSave of the MessageRequest it is checked if the message is direct or public and based on that
in case of direct message - one new message object of the Message class is created and saved
in case of public announcement - for each user except author, a new message object is created and added to a list of messages, then the list is saved
In both cases, the data like content, type, etc. are copied from messageRequest object into the newly created message object(s).
The reason for creating separate message for each user is that each user can have it in another status (unread, read, deleted).
The status column representing the unread, read, deleted status is set (by unread) for the message object.
Problem
When I call the ParseObject.saveAll method in the afterSave of MessageRequest, I get the Execution timed out - Request timed out error
I think the cause is that there are some limits on time in which the request must complete
in cloud code. In my case, I'm creating ca 100 Messages for 1 MessageRequest
This doesn't seem so much to me, but maybe I'm wrong.
Source code
var generateAnnouncement = function(messageRequest, recipients) {
var messageList = [];
for (var i = 0; i < recipients.length; i++) {
var msg = new Message();
msg.set("type", 1);
msg.set("author", messageRequest.get("author"));
msg.set("content", messageRequest.get("content"));
msg.set("recipient", recipients[i]);
msg.set("status", 0)
messageList.push(msg);
}
Parse.Object.saveAll(messageList).then(function(list) {
}, function(error) {
console.error(error.message);
});
}
Parse.Cloud.afterSave("MessageRequest", function(request) {
var mr = request.object;
var type = mr.get("type");
if (type == 0) {
generateDirectMessage(mr);
} else {
var query = new Parse.Query(Parse.User);
query.notEqualTo("objectId", mr.get("author").id);
query.find().then(function(allUsersExceptAuthor) {
generateAnnouncement(mr, allUsersExceptAuthor);
}, function(error) {
console.error(error.message);
});
}
});
How would you suggest to solve this?
Additional thoughts
My only other idea how to solve this is to have only one Message object, and two columns called e.g. viewedBy and deletedFor which would contain lists of users that already viewed the message or have delete the message for them.
In this case, I'm not very sure about the performance of the queries
Also, I know, many of you think Why isn't he using table for splitting the M:N relation between the MessageRequest(which could be actually called Message in that case) and User?
My answer is that I had this solution, but it was harder to work with it in the Android code, more pointers, more includes in queries, etc.
Moreover, I would have to create the same amount of objects representing status for each user in the on parse.com back-end anyway, so I think the problem with Execution time out would be the same in the end
Update - mockup representing user's "Inbox"
In the "inbox" user sees both direct messages and public announcements. They are sorted by chronological order.
Update #2 - using arrays to identify who viewed and who marked as deleted
I have just one Message object, via type I identify if it is direct or public
Two array columns were added
viewedBy - containing users that already viewed the message
deletedFor - containing users that marked the message as deleted for them
Then my query for all messages not deleted by currently logged in user looks like this
//direct messages for me
ParseQuery<Message> queryDirect = ParseQuery.getQuery(Message.class);
queryDirect.whereEqualTo("type", 0);
queryDirect.whereEqualTo("recipient", ParseUser.getCurrentUser());
//public announcements
ParseQuery<Message> queryAnnouncements = ParseQuery.getQuery(Message.class);
queryAnnouncements.whereEqualTo("type", 1);
//I want both direct and public
List<ParseQuery<Message>> queries = new ArrayList<ParseQuery<Message>>();
queries.add(queryDirect);
queries.add(queryAnnouncements);
ParseQuery<Message> queryMessages = ParseQuery.or(queries);
//... but only those which I haven't deleted for myself
queryMessages.whereNotEqualTo("deletedFor", ParseUser.getCurrentUser());
//puting them in correct order
queryMessages.addDescendingOrder("createdAt");
//and attaching the author ParseUser object (to get e.g. his name or URL to photo)
queryMessages.include("author");
queryMessages.findInBackground(new FindCallback<Message>() {/*DO SOMETHING HERE*/});
I would suggest changing your schema to better support public messages.
You should have a single copy of the public message, as there's no changing the message itself.
You should then store just the status for each user if it is anything other than "unread". This would be another table.
When a MessageRequest comes in with type 1, create a new PublicMessage, don't create any status rows as everyone will use the default status of "unread". This makes your afterSave handler work cleanly as it is always creating just one new object, either a Message or a PublicMessage.
As each user reads the message or deletes it, create new PublicMessageStatus row for that user with the correct status.
When showing public messages to a user, you will do two queries:
Query for PublicMessage, probably with some date range
Query for PublicMessageStatus with a filter on user matching the current user and matchesQuery('publicMessage', publicMessageQuery) constraint using a clone of the first query
Client side you'll then need to combine the two to hide/remove those with status "deleted" and mark those with status "read" accordingly.
Update based on feedback
You could choose instead to use a single Message class for public/private messages, and a MessageStatus class to handle status.
Public vs Private would be based on the Message.recipient being empty or not.
To get all messages for the current user:
// JavaScript sample since you haven't specified a language
// assumes Underscore library available
var Message = Parse.Object.extend('Message');
var MessageStatus = Parse.Object.extend('MessageStatus');
var publicMessageQuery = new Parse.Query(Message);
publicMessageQuery.doesNotExist('recipient');
publicMessageQuery.notEqualTo('author', currentUser);
var privateMessageQuery = new Parse.Query(Message);
privateMessageQuery.equalTo('recipient', currentUser);
var messagesQuery = new Parse.Query.or(publicMessageQuery, privateMessageQuery);
messagesQuery.descending('createdAt');
// set any other filters to apply to both queries
var messages = [];
messageQuery.find().then(function(results) {
messages = _(results).map(function (message) {
return { message: message, status: 'unread', messageId: message.objectId };
});
var statusQuery = new Parse.Query(MessageStatus);
statusQuery.containedIn('message', results);
statusQuery.equalTo('user', currentUser);
// process status in order so last applies
statusQuery.ascending('createdAt');
return
}).then(function(results) {
_(results).each(function (messageStatus) {
var messageId = messageStatus.get('message').objectId;
_(messages).findWhere({ messageId: messageId }).status = messageStatus.get('status');
});
});
// optionally filter messages that are deleted
messages = _(messages).filter(function(message) { return message.status !== 'deleted'; });
// feed messages array to UI...

Resources