I've been breaking my head over this the whole day.
I wish to integrate my iOS app with Withings api. It uses OAuth 1.0 and I can't seem to understand fully how to implement it.
I've been downloading multiple OAuth framworks (MPOAuth,gtm-oauth,ssoauthkit) but couldn't figure out completely what exactly I should do.
I searched a lot, also in stack overflow for good references on how to go about implementing OAuth 1.0 in general & integrating with Withings in particular with no success.
Kindly explain the flow of integrating an iOS app with an api that requires OAuth 1.0. Code examples would be very helpful. Suggested 3rd party frameworks would be nice too.
Just to clarify, I fully understand the OAuth 1.0 principles, I just have problems in actually implementing it in my app.
I think that a thorough answer with code examples and good references would be very helpful for lots of people as I couldn't find one. If anyone has good experience with implementing it, please take the time to share it.
TDOAuth in my opinion was the best solution. it is clean and simple, only one .h and .m file to work with, and no complicated example projects..
This is the OAuth 1.0 flow:
step 1 - get request token
//withings additional params
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:CALL_BACK_URL forKey:#"oauth_callback"];
//init request
NSURLRequest *rq = [TDOAuth URLRequestForPath:#"/request_token" GETParameters:dict scheme:#"https" host:#"oauth.withings.com/account" consumerKey:WITHINGS_OAUTH_KEY consumerSecret:WITHINGS_OAUTH_SECRET accessToken:nil tokenSecret:nil];
//fire request
NSURLResponse* response;
NSError* error = nil;
NSData* result = [NSURLConnection sendSynchronousRequest:rq returningResponse:&response error:&error];
NSString *s = [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding];
//parse result
NSMutableDictionary *params = [NSMutableDictionary dictionary];
NSArray *split = [s componentsSeparatedByString:#"&"];
for (NSString *str in split){
NSArray *split2 = [str componentsSeparatedByString:#"="];
[params setObject:split2[1] forKey:split2[0]];
}
token = params[#"oauth_token"];
tokenSecret = params[#"oauth_token_secret"];
step 2 - get authorize token (by loading the request in a UIWebView, the webViewDidFinishLoad delegate method will handle the call back..)
//withings additional params
NSMutableDictionary *dict2 = [NSMutableDictionary dictionary];
[dict setObject:CALL_BACK_URL forKey:#"oauth_callback"];
//init request
NSURLRequest *rq2 = [TDOAuth URLRequestForPath:#"/authorize" GETParameters:dict2 scheme:#"https" host:#"oauth.withings.com/account" consumerKey:WITHINGS_OAUTH_KEY consumerSecret:WITHINGS_OAUTH_SECRET accessToken:token tokenSecret:tokenSecret];
webView.delegate = self;
[DBLoaderHUD showDBLoaderInView:webView];
[webView loadRequest:rq2];
handle the webView as follow to initiate step 3 (I know the isAuthorizeCallBack smells a lot, but it does the job, should refactor it..)
- (void)webViewDidFinishLoad:(UIWebView *)aWebView
{
[DBLoaderHUD hideDBLoaderInView:webView];
NSString *userId = [self isAuthorizeCallBack];
if (userId) {
//step 3 - get access token
[DBLoaderHUD showDBLoaderInView:self.view];
[self getAccessTokenForUserId:userId];
}
//ugly patchup to fix an invalid token bug
if ([webView.request.URL.absoluteString isEqualToString:#"http://oauth.withings.com/account/authorize?"])
[self startOAuthFlow];
}
- (NSString *)isAuthorizeCallBack
{
NSString *fullUrlString = webView.request.URL.absoluteString;
if (!fullUrlString)
return nil;
NSArray *arr = [fullUrlString componentsSeparatedByString:#"?"];
if (!arr || arr.count!=2)
return nil;
if (![arr[0] isEqualToString:CALL_BACK_URL])
return nil;
NSString *resultString = arr[1];
NSArray *arr2 = [resultString componentsSeparatedByString:#"&"];
if (!arr2 || arr2.count!=3)
return nil;
NSString *userCred = arr2[0];
NSArray *arr3 = [userCred componentsSeparatedByString:#"="];
if (!arr3 || arr3.count!=2)
return nil;
if (![arr3[0] isEqualToString:#"userid"])
return nil;
return arr3[1];
}
- (void)startOAuthFlow
{
[self step1];
[self step2];
}
and finally - step 3 - get access token
- (void)getAccessTokenForUserId:(NSString *)userId
{
//step 3 - get access token
//withings additional params
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:CALL_BACK_URL forKey:#"oauth_callback"];
[dict setObject:userId forKey:#"userid"];
//init request
NSURLRequest *rq = [TDOAuth URLRequestForPath:#"/access_token" GETParameters:dict scheme:#"https" host:#"oauth.withings.com/account" consumerKey:WITHINGS_OAUTH_KEY consumerSecret:WITHINGS_OAUTH_SECRET accessToken:token tokenSecret:tokenSecret];
//fire request
NSURLResponse* response;
NSError* error = nil;
NSData* result = [NSURLConnection sendSynchronousRequest:rq returningResponse:&response error:&error];
NSString *s = [[NSString alloc] initWithData:result encoding:NSUTF8StringEncoding];
//parse result
NSMutableDictionary *params = [NSMutableDictionary dictionary];
NSArray *split = [s componentsSeparatedByString:#"&"];
for (NSString *str in split){
NSArray *split2 = [str componentsSeparatedByString:#"="];
[params setObject:split2[1] forKey:split2[0]];
}
[self finishedAthourizationProcessWithUserId:userId AccessToken:params[#"oauth_token"] AccessTokenSecret:params[#"oauth_token_secret"]];
}
I additionaly save request headers here
NSMutableDictionary *dict2 = [NSMutableDictionary dictionary];
[dict2 setObject:CALL_BACK_URL forKey:#"oauth_callback"];
NSURLRequest *rq2 = [TDOAuth URLRequestForPath:#"/authorize"
GETParameters:dict2
scheme:#"https"
host:#"oauth.withings.com/account"
consumerKey:WITHINGS_OAUTH_KEY
consumerSecret:WITHINGS_OAUTH_SECRET
accessToken:self.token
tokenSecret:self.tokenSecret];
headers = rq2.allHTTPHeaderFields;
And in callback method i will add missing parameters to the request. By doing it this way, i avoid "ugly patchup fix".
- (BOOL)webView:(UIWebView *)wV shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
if (![request.allHTTPHeaderFields objectForKey:#"Authorization"] &&
[request.URL.absoluteString rangeOfString:#"acceptDelegation=true"].location == NSNotFound){
NSMutableURLRequest *mutableCp = [request mutableCopy];
NSLog(#"request :::%#", request);
[mutableCp setAllHTTPHeaderFields:headers];
dispatch_async(dispatch_get_main_queue(), ^{
[webView loadRequest:mutableCp];
});
return NO;
}
return YES;
}
I hope it will help somebody
I would suggest you to check this project both as a reference and as a really working OAuth class. It inherits from another great project, so you you will need to add both in yours.Check if the license will suits your requirements.
https://github.com/rsieiro/RSOAuthEngine
Related
I have a code that accesses a Twitter feed and puts the text into a table. I then edited the code so I could display the text in my custom fashion in separate views, but I wanted to grab images from the tweets as well, and despite over an hour searching could not find a single reference. I have seen how to "Post" images, but to be clear, I need to get and "display" the images from the tweet in question.
Here are the highlights from my code that handles the Twitter Access:
-(void)twitterTimeLine
{
ACAccountStore *account = [[ACAccountStore alloc] init];
ACAccountType *accountType = [account accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierTwitter];
[account requestAccessToAccountsWithType:accountType options:nil completion:^(BOOL granted, NSError *error) {
if (granted == YES)
{
NSArray *arrayOfAccounts = [account accountsWithAccountType:accountType];
if ([arrayOfAccounts count] > 0)
{
ACAccount *twitterAccount = [arrayOfAccounts lastObject]; // last account on list of accounts
NSURL *requestAPI = [NSURL URLWithString:#"https://api.twitter.com/1.1/statuses/user_timeline.json"];
NSMutableDictionary *parameters = [[NSMutableDictionary alloc] init];
[parameters setObject:#"30" forKey:#"count"];
[parameters setObject:#"1" forKey:#"incude_entities"];
SLRequest *posts = [SLRequest requestForServiceType:SLServiceTypeTwitter requestMethod:SLRequestMethodGET URL:requestAPI parameters:parameters];
posts.account = twitterAccount;
[posts performRequestWithHandler:^(NSData *response, NSHTTPURLResponse *urlResponse, NSError *error) {
if (response)
{
// TODO: might want to check urlResponse.statusCode to stop early
NSError *jsonError; // use new instance here, you don't want to overwrite the error you got from the SLRequest
NSArray *array =[NSJSONSerialization JSONObjectWithData:response options:NSJSONReadingMutableLeaves error:&jsonError];
if (array) {
if ([array isKindOfClass:[NSArray class]]) {
self.array = array;
NSLog(#"resulted array: %#",self.array);
}
else {
// This should never happen
NSLog(#"Not an array! %# - %#", NSStringFromClass([array class]), array);
}
}
else {
// TODO: Handle error in release version, don't just dump out this information
NSLog(#"JSON Error %#", jsonError);
NSString *dataString = [[NSString alloc] initWithData:response encoding:NSUTF8StringEncoding];
NSLog(#"Received data: %#", dataString ? dataString : response); // print string representation if response is a string, or print the raw data object
}
}
else {
// TODO: show error information to user if request failed
NSLog(#"request failed %#", error);
}
self.array = [NSJSONSerialization JSONObjectWithData:response options:NSJSONReadingMutableLeaves error:&error];
if (self.array.count != 0)
{
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData]; // this part loads into table - important!
});
}
}];
}
}
else
{
NSLog(#"%#", [error localizedDescription]);
}
}];
}
and here is how I display the Tweet
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellID = #"cellID";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (cell == nil)
{
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
}
NSDictionary *tweet = _array[indexPath.row];
cell.textLabel.text = tweet[#"text"];
//NSString *element = [myArray objectAtIndex:2];
//NSString *element = myArray[2];
// I created some custom views to show the text, but kept the table for testing purposes
TemplateView *tempView = [viewArray objectAtIndex:testCounter];
tempView.TweetView.text = tweet[#"text"];
// -> this was what I was hoping for // tempView.ContentView.image = tweet[#"image"];
testCounter++;
if (testCounter >= 30)
{
testCounter = 0;
}
return cell;
}
I took out the key lines that I think is where I need to look:
tempView.TweetView.text = tweet[#"text"];
tempView.ContentView.image = tweet[#"image"];
// hoping that the latter would work as the first one does, but clearly it's not that simple
This might not be possible, if so, how would I get the images from the "link" (url) and make sure it is an image and not a video or other website?
I could set up a "word search" to grab text starting with http from the tweet and hopefully generate a URL from the string
TwitterKit doesn't seem to support images publicly.. I'm having the same stupid issue. The API internally holds images when using the built in tableview and datasource.. It requires a listID and a Slug.. However, when you want the images via JSON, you are out of luck! Even TWTRTweet object doesn't have entities or media properties!
Not sure how anyone can develop such an awful API.. In any case, I reversed the server calls made internally and found that it sends other "undocumented" parameters..
Example:
TWTRAPIClient *client = [[TWTRAPIClient alloc] init];
NSString *endpoint = #"https://api.twitter.com/1.1/statuses/user_timeline.json";
NSDictionary *params = #{#"screen_name":#"SCREEN_NAME_HERE",
#"count": #"30"};
will return NO MEDIA.. even if you have #"include_entities" : #"true".
Solution:
TWTRAPIClient *client = [[TWTRAPIClient alloc] init];
NSString *endpoint = #"https://api.twitter.com/1.1/statuses/user_timeline.json";
NSDictionary *params = #{#"screen_name":#"SCREEN_NAME_HERE",
#"count": #"30",
#"tweet_mode": #"extended"};
With tweet_mode set to extended (tweet_mode is an undocumented parameter), it will now return the media as part of the response.. This includes the "type" which is "photo" for images.
We can get tweets with images by applying "filter=images" query with "include_entities=true". It'll give tweets with media entities, under which we can see type="photo" and other related data.
For ex:
https://api.twitter.com/1.1/search/tweets.json?q=nature&include_entities=true&filter=images
Try this query at twitter developer console and see the response format: https://dev.twitter.com/rest/tools/console
Hope this will help.
When using JSON.parse(parameter-value) in JavaScript, the adapter invocation is working correctly, however when doing similarly in a native iOS app, it is failing with the following error.
Javascript Adapter Call:
var invocationData = {
adapter : 'TEST_ADAP',
procedure : 'PROC1',
parameters : [JSON.parse(A)],
};
Native Call:
json= // some json value will be come
MyConnect *connectListener = [[MyConnect alloc] initWithController:self];
[[WLClient sharedInstance] wlConnectWithDelegate:connectListener];
WLProcedureInvocationData *myInvocationData = [[WLProcedureInvocationData alloc] initWithAdapterName:#"TEST" procedureName:#"test"];
myInvocationData.parameters = [NSArray arrayWithObjects:json, nil];
for (NSString *str in myInvocationData.parameters) {
NSLog(#"values of account test %#",str);
}
PasswardPage *invokeListener = [[PasswardPage alloc] initWithController:self];
[[WLClient sharedInstance] invokeProcedure:myInvocationData withDelegate:invokeListener];
Your line
myInvocationData.parameters = [NSArray arrayWithObjects:json, nil];
is almost right.
The parameters property should be an NSArray (as you did) but the array must be made of string values - NOT a JSON object.
myInvocationData.parameters = [NSArray arrayWithObjects:#"myValue1", #"myValue2", #"myValue3", nil];
If the data you received is not in this format, you need to first convert it to this format. This is out of the scope of this question.
If you are not sure how to convert your existing format into a valid NSArray, please open a new question (tagged with Objective-C, not worklight).
We can pass JSON as NSString in iOS Native code to invoke adapter
Example
//Created Dictionary
NSMutablec *dict = [[NSMutableDictionary alloc]init];
[dict setObject:#"xyz" forKey:#"Name"];
[dict setObject:#"iOS" forKey:#"Platform"];
//Convert it to JSON Data
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dict
options:nil
error:&error];
//JSON Data To NSString
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
WLProcedureInvocationData * invocationData = [[WLProcedureInvocationData alloc] initWithAdapterName:#"XYZAdapter" procedureName:#"FunctionXYZ"];
//Passing jsonString (NSString Created out of JSON Data) as array to set Parameters.
[invocationData setParameters:[NSArray arrayWithObject:jsonString]];
[[WLClient sharedInstance] invokeProcedure:invocationData withDelegate:self];
I'm trying to create an order from my IOS app to my Shopify site.
This is what the documentation says I should do.
Create a simple order with only a product variant id.
POST /admin/orders.json
{
"order": {
"line_items": [
{
"variant_id": 447654529,
"quantity": 1
}
]
}
}
It does not say much more.
Here is what I got.
<code>
NSMutableDictionary *lineItem1=[[NSMutableDictionary alloc]init];
[lineItem1 setObject:#"1125533997" forKeyedSubscript:#"variant_id"];
[lineItem1 setObject:#"1" forKeyedSubscript:#"quantity"];
NSMutableArray *lineItems=[[NSMutableArray alloc]init];
[lineItems addObject:lineItem1];
NSMutableDictionary *orders=[[NSMutableDictionary alloc]init];
[orders setObject:lineItems forKeyedSubscript:#"line_items"];
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:orders options:NSJSONWritingPrettyPrinted error:&error];
NSString *myString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
myString=[JuicyApi md5HexDigest:myString];
//Set parameter
NSMutableDictionary *params = [[NSMutableDictionary alloc]init];
[params setObject:myString forKeyedSubscript:#"order"];
//Generate the request with the give settings
NSMutableURLRequest *req = [self getRequestWithFunction:#"admin/orders.json" requestType:#"POST" params:params ssl:true];
</code>
The server is giving me a response that says.
<code>
{"errors":{"order":"expected String to be a Hash"}}
</code>
I tried hashing all of it, only the values, in this example everything in order, can't get it to work. Am I hashing it incorrectly?
What am I missing here?
I also had the same issue. I got fixed by setting the proper header before making the request
setHeader("Content-Type", "application/json")
I am working on project in which we are displaying local business search. I am using YELP to search local business. As per YELP Documentation i have created query. But it gives result based on location only.
I am trying with Google Place API but not getting desired result.
My YELP request - http://api.yelp.com/v2/search/?term=restaurant&location=nyc&limit=20&offset=1
My Google Place API request - https://maps.googleapis.com/maps/api/place/textsearch/json?query=hotels+in+nyc&sensor=true&key=AIzaSyCHwd5OgRXdeuTWV46SHdMLq2lXL20t22U
How can i get result by business name & location as well using any YELP or Google Place API?
Which one is better to use YELP or Google Place API?
1) I used Yelp API. Url for special business - http://api.yelp.com/v2/business/
For global search - http://api.yelp.com/v2/search
After search you must correctly pass data in api search url. Notice of url signature in NSStringWithFormat. And don't forget OAuth keys! My request:
-(void)searchBy:(NSString *)categoryFilter inLocationCity:(NSString *)aLocationCity {
NSString *urlString = [NSString stringWithFormat:#"%#?term=%#&location=%#",
YELP_SEARCH_URL,
categoryFilter,
aLocationCity];
NSURL *URL = [NSURL URLWithString:[urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
OAConsumer *consumer = [[OAConsumer alloc] initWithKey:OAUTH_CONSUMER_KEY
secret:OAUTH_CONSUMER_SECRET];
OAToken *token = [[OAToken alloc] initWithKey:OAUTH_TOKEN
secret:OAUTH_TOKEN_SECRET];
id<OASignatureProviding, NSObject> provider = [[OAHMAC_SHA1SignatureProvider alloc] init];
NSString *realm = nil;
OAMutableURLRequest *request = [[OAMutableURLRequest alloc] initWithURL:URL
consumer:consumer
token:token
realm:realm
signatureProvider:provider];
[request prepare];
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
if (conn) {
self.urlRespondData = [NSMutableData data];
}
}
Also add methods NSURLConnectionDelegate:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[self.urlRespondData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)d {
[self.urlRespondData appendData:d];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSError *e = nil;
NSDictionary *resultResponseDict = [NSJSONSerialization JSONObjectWithData:self.urlRespondData
options:NSJSONReadingMutableContainers
error:&e];
if (self.resultArray && [self.resultArray count] > 0){
[self.resultArray removeAllObjects];
}
if (!self.resultArray) {
self.resultArray = [[NSMutableArray alloc] init];
}
DLog(#"YELP response %#", resultResponseDict);
if (resultResponseDict && [resultResponseDict count] > 0) {
if ([resultResponseDict objectForKey:#"businesses"] &&
[[resultResponseDict objectForKey:#"businesses"] count] > 0) {
for (NSDictionary *venueDict in [resultResponseDict objectForKey:#"businesses"]) {
Venue *venueObj = [[Venue alloc] initWithDict:venueDict];
[self.resultArray addObject:venueObj];
}
}
}
[self.delegate loadResultWithDataArray:self.resultArray];
}
-(instancetype)initWithDict:(NSDictionary *)dict {
self = [super init];
if (self) {
self.name = [dict objectForKey:#"name"];
self.venueId = [dict objectForKey:#"id"];
self.thumbURL = [dict objectForKey:#"image_url"];
self.ratingURL = [dict objectForKey:#"rating_img_url"];
self.yelpURL = [dict objectForKey:#"url"];
self.venueId = [dict objectForKey:#"id"];
self.reviewsCount =[[dict objectForKey:#"review_count"] stringValue];
self.categories = [dict objectForKey:#"categories"][0][0];
self.distance = [dict objectForKey:#"distance"];
self.price = [dict objectForKey:#"deals.options.formatted_price"];
self.address = [[[dict objectForKey:#"location"] objectForKey:#"address"] componentsJoinedByString:#", "];
NSArray *adr = [[dict objectForKey:#"location"] objectForKey:#"display_address"];
self.displayAddress = [adr componentsJoinedByString:#","];
}
return self;
}
Method with yelp response values...You need just id. Coordinates need for you location...When you get some venues see theirs id with Log or print.
I solve my problem using Google Places API -
Thanks to This Answer.
We get JSON/XML response
Search hotels near City:
https://maps.googleapis.com/maps/api/place/textsearch/json?query=hotels+in+Pune&sensor=true&key=AddYourOwnKeyHere
Search specific place in city:
https://maps.googleapis.com/maps/api/place/textsearch/json?query=[SearchPlaceName]+in+[CityName]&sensor=true&key=AddYourOwnKeyHere
Search specific place in city by given type:
https://maps.googleapis.com/maps/api/place/textsearch/json?query=[SearchPlaceName]+in+[CityName]&type=[PlaceType]&sensor=true&key=AddYourOwnKeyHere
To retrieve image/icons for restaurant/place -
As per Documentation.
We can use photo_reference & request like -
https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=CoQBegAAAFg5U0y-iQEtUVMfqw4KpXYe60QwJC-wl59NZlcaxSQZNgAhGrjmUKD2NkXatfQF1QRap-PQCx3kMfsKQCcxtkZqQ&key=AddYourOwnKeyHere
There is a JSON file on server, which contain four "key/value" pairs about version of data in app (it is a cook book, so the version is a version of recipes)
When application starts, it download JSON and check version.
Here is my method, but I think it is very slow.
- (void)isUpdatesAvail
{
updatesAvail = NO;
NSInteger iVer = [[NSUserDefaults standardUserDefaults] integerForKey:#"ingredientsVer"];
NSInteger rVer = [[NSUserDefaults standardUserDefaults] integerForKey:#"recipesVer"];
NSInteger iCnt = [[NSUserDefaults standardUserDefaults] integerForKey:#"ingredientsCount"];
NSInteger rCnt = [[NSUserDefaults standardUserDefaults] integerForKey:#"recipesCount"];
NSString *countPath = [downloadPath stringByAppendingString:#"/versioninfo"];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:countPath]];
NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:nil error:nil];
NSError *error = nil;
if (!responseData) {
return;
}
NSDictionary *recipesCount = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:&error];
if (!recipesCount) {
return;
}
rCount = [recipesCount objectForKey:#"recipeCount"];
iCount = [recipesCount objectForKey:#"ingredientCount"];
rVersion = [recipesCount objectForKey:#"recipeVersion"];
iVersion = [recipesCount objectForKey:#"ingredientVersion"];
if ([iVersion integerValue] > iVer || [rVersion integerValue] > rVer || [iCount integerValue] > iCnt || [rCount integerValue] > rCnt) {
updatesAvail = YES;
}
}
Can somebody give me advice (or may be example) how to do that?
It's 'slow' because it performs a synchronous URL request. It's probably just waiting for the network transfer to complete for most of the time it executes if it is noticeably slow.
There's not much you can do to make it faster, apart from either caching the data or reducing the size of the data.
In any event, you should just perform an asynchronous request then do your work/update after it's completed.