We have a native iOS version done in Objective-c and we have developed a new version of this app in Flutter. This new Flutter version has a plugin created in Objective-c to get Realm data from native app (old version) and imported to the new Flutter version. And this works!!
Now we want to add the ability to get the token of the old app (using A0SimpleKeychain) and save it in the new one (using Flutter Secure Storage package).
We have this code but A0SimpleKeychain returns null:
// Get the current token stored in keychain
- (NSString *) getCurrentToken {
A0SimpleKeychain *keychain = [[A0SimpleKeychain alloc] initWithService:#"Auth0"];
NSString *token = #"";
token = [[A0SimpleKeychain keychain] stringForKey:#"auth0-user-jwt"];
NSLog(#"TOKEN = %#", token);
return token;
}
Any ideas?
Related
I would like to use dialogflow service in the app written using obj-c. Have been using api.ai library for a while but could not seem to find a library for obj-c for dialogflow v2(beta1) apis. My agent is upgraded to v2 already, but the api.ai internally is using /v1/ endpoints and I need to use v2beta1 specific features like access to knowledge bases. (https://cloud.google.com/dialogflow/docs/reference/rpc/google.cloud.dialogflow.v2beta1#queryparameters - knowledge_base_names).
The dialogflow api is a standard REST API, so all I need to have is OAuth2.0 & REST client, but coding this sounds like re-inventing the wheel.
Please advice. Thank you
I don't think there's a library written specifically for Dialogflow v2; however, the library google-api-objectivec-client-for-rest is a generic library provided by Google, that simplifies the code to consume their Rest APIs.
This library is updated to be used with Dialogflow V2. In order to use it, you'll need to match the Rest API, with the "Queries" (API methods) and "Objects" (API types) in the library, which is not that difficult because the names are basically the same.
For example, the detectIntent method full name is:
projects.agent.sessions.detectIntent
In the library, it is the equivalent to the Query:
GTLRDialogflowQuery_ProjectsAgentSessionsDetectIntent
Here's an example of a detectIntent request:
// Create the service
GTLRDialogflowService *service = [[GTLRDialogflowService alloc] init];
// Create the request object (The JSON payload)
GTLRDialogflow_GoogleCloudDialogflowV2DetectIntentRequest *request =
[GTLRDialogflow_GoogleCloudDialogflowV2DetectIntentRequest object];
// Set the information in the request object
request.inputAudio = myInputAudio;
request.outputAudioConfig = myOutputAudioConfig;
request.queryInput = myQueryInput;
request.queryParams = myQueryParams;
// Create a query with session (Path parameter) and the request object
GTLRDialogflowQuery_ProjectsAgentSessionsDetectIntent *query =
[GTLRDialogflowQuery_ProjectsAgentSessionsDetectIntent queryWithObject:request
session:#"session"];
// Create a ticket with a callback to fetch the result
GTLRServiceTicket *ticket =
[service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket,
GTLRDialogflow_GoogleCloudDialogflowV2DetectIntentResponse *detectIntentResponse,
NSError *callbackError) {
// This callback block is run when the fetch completes.
if (callbackError != nil) {
NSLog(#"Fetch failed: %#", callbackError);
} else {
// The response from the agent
NSLog(#"%#", detectIntentResponse.queryResult.fulfillmentText);
}
}];
You can find more information and samples, in the library wiki. Finally, the library also has a sample code using Google Cloud Storage which ilustrates its use with GCP services.
I think that without a specific library for Dialogflow V2, this might be the next thing to try before implementing it from scratch.
EDIT
Oops, I was missing the fact that the generated service for Dialogflow does not contain v2beta1.
In this case, it is needed an additional first step, which is to use the Dialogflow v2beta1 DiscoveryDocument and the ServiceGenerator, to create the service interface for v2beta1. Then you can continue working the same as I mentioned before.
Following #Tlaquetzal example, I ended up doing something like below
In pod file
pod 'GoogleAPIClientForREST'
pod 'JWT'
Using ServiceGenerator and Discovery Document as mentioned above, generated set of DialogFlow v2beta1 classes. Command line
./ServiceGenerator --outputDir . --verbose --gtlrFrameworkName GTLR --addServiceNameDir yes --guessFormattedNames https://dialogflow.googleapis.com/\$discovery/rest?version=v2beta1
And added them to the project.
#import "DialogflowV2Beta1/GTLRDialogflow.h"
Next step is to generate JWT Token. I have used this library JSON Web Token implementation in Objective-C. I want to connect using a service account.
NSInteger unixtime = [[NSNumber numberWithDouble: [[NSDate date] timeIntervalSince1970]] integerValue];
NSInteger expires = unixtime + 3600; //expire in one hour
NSString *iat = [NSString stringWithFormat:#"%ld", unixtime];
NSString *exp = [NSString stringWithFormat:#"%ld", expires];
NSDictionary *payload = #{
#"iss": #"<YOUR-SERVICE-ACCOUNT-EMAIL>",
#"sub": #"<YOUR-SERVICE-ACCOUNT-EMAIL>",
#"aud": #"https://dialogflow.googleapis.com/",
#"iat": iat,
#"exp": exp
};
NSDictionary *headers = #{
#"alg" : #"RS256",
#"typ" : #"JWT",
#"kid" : #"<KID FROM YOUR SERVICE ACCOUNT FILE>"
};
NSString *algorithmName = #"RS256";
NSData *privateKeySecretData = [[[NSDataAsset alloc] initWithName:#"<IOS-ASSET-NAME-JSON-SERVICE-ACCOUNT-FILE>"] data];
NSString *passphraseForPrivateKey = #"<PASSWORD-FOR-PRIVATE-KEY-IN-CERT-JSON>";
JWTBuilder *builder = [JWTBuilder encodePayload:payload].headers(headers).secretData(privateKeySecretData).privateKeyCertificatePassphrase(passphraseForPrivateKey).algorithmName(algorithmName);
NSString *token = builder.encode;
// check error
if (builder.jwtError == nil) {
JwtToken *jwtToken = [[JwtToken alloc] initWithToken:token expires:expires];
success(jwtToken);
}
else {
// error occurred.
MSLog(#"ERROR. jwtError = %#", builder.jwtError);
failure(builder.jwtError);
}
When token is generated, it can be used for an hour (or time you specify above).
To make a call to dialogflow you need to define your project path. To create a project path for the call, append to the code below your unique session identifier. Session is like a conversation for dialogflow, so different users should use different session ids
#define PROJECTPATH #"projects/<YOUR-PROJECT-NAME>/agent/sessions/"
Making dialogflow call
// Create the service
GTLRDialogflowService *service = [[GTLRDialogflowService alloc] init];
//authorise with token
service.additionalHTTPHeaders = #{
#"Authorization" : [NSString stringWithFormat:#"Bearer %#", self.getToken.token]
};
// Create the request object (The JSON payload)
GTLRDialogflow_GoogleCloudDialogflowV2beta1DetectIntentRequest *request = [GTLRDialogflow_GoogleCloudDialogflowV2beta1DetectIntentRequest object];
//create query
GTLRDialogflow_GoogleCloudDialogflowV2beta1QueryInput *queryInput = [GTLRDialogflow_GoogleCloudDialogflowV2beta1QueryInput object];
//text query
GTLRDialogflow_GoogleCloudDialogflowV2beta1TextInput *userText = [GTLRDialogflow_GoogleCloudDialogflowV2beta1TextInput object];
userText.text = question;
userText.languageCode = LANGUAGE;
queryInput.text = #"YOUR QUESTION TO dialogflow agent"; //userText;
// Set the information in the request object
//request.inputAudio = myInputAudio;
//request.outputAudioConfig = myOutputAudioConfig;
request.queryInput = queryInput;
//request.queryParams = myQueryParams;
//Create API project path with session
NSString *pathAndSession = [NSString stringWithFormat:#"%#%#", PROJECTPATH, [self getSession]];
// Create a query with session (Path parameter) and the request object
GTLRDialogflowQuery_ProjectsAgentSessionsDetectIntent *query = [GTLRDialogflowQuery_ProjectsAgentSessionsDetectIntent queryWithObject:request session:pathAndSession];
// Create a ticket with a callback to fetch the result
// GTLRServiceTicket *ticket =
[service executeQuery:query
completionHandler:^(GTLRServiceTicket *callbackTicket, GTLRDialogflow_GoogleCloudDialogflowV2beta1DetectIntentResponse *detectIntentResponse, NSError *callbackError) {
// This callback block is run when the fetch completes.
if (callbackError != nil) {
NSLog(#"error");
NSLog(#"Fetch failed: %#", callbackError);
//TODO: Register failure with analytics
failure( callbackError );
}
else {
// NSLog(#"Success");
// The response from the agent
// NSLog(#"%#", detectIntentResponse.queryResult.fulfillmentText);
NSString *response = detectIntentResponse.queryResult.fulfillmentText;
success( response );
}
}];
This is a basic implementation, but works and good for demo.
Good luck
Our team was using Ionic/Cordova with IBM worklight(7.0). Recently we got an opportunity to try out react-native and we need the worklight integrated with the application.
How do we integrate IBM worklight with react-native application?
In general you need you to create a React Native project like any other, and add to it the MobileFirst Native iOS SDK as you also would add it to any other native iOS project (by following these instructions: https://mobilefirstplatform.ibmcloud.com/tutorials/en/foundation/7.0/hello-world/configuring-a-native-ios-with-the-mfp-sdk/)
You can find detailed instrustions and a video in the following blog post: https://mobilefirstplatform.ibmcloud.com/blog/2015/06/03/react-native/
The blog post does mention an important aspect of the integration to take note of:
Below I think one of the more important chunks of code to integrate React Native for iOS with the MobileFirst Platform Foundation. The code chunk is the MobileFirst Platform Foundation Adapter invocation call from the file ResourceRequest.m being exported into the React Native javascript code. In this section of code there are two important React Native methods. One being RCT_EXPORT_MODULE(); which will allow you to export a native class into the React Native javascript. The other is RCT_EXPORT_METHOD(...) this class explicitly tells React to expose this class's method for use in the React Native javascript. Exporting the particular method below is going to pass in a path named "path" and also a callback named "results." The adapter call is getting a list of movies stored as JSON data to a particular path (this path ended up not being needed). Then that data is passed as the callback into the React Native code.
#import <Foundation/Foundation.h>
#import "WLResourceRequest.h"
#import "ResourceRequest.h"
#implementation ResourceRequest
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(getJavaAdapter:(NSString *)path results:(RCTResponseSenderBlock)callback)
{
NSLog(#"Invoking GET Procedure...");
NSURL* url = [NSURL URLWithString:#"http://localhost:10080/HelloMobileFirst/adapters/MoviesAdapter/getStories"];
WLResourceRequest* resourceRequest = [WLResourceRequest requestWithURL:url method:WLHttpMethodGet];
[resourceRequest sendWithCompletionHandler:^(WLResponse *response, NSError *error) {
NSString* resultText;
if(error != nil){
resultText = #"Invocation failure.";
resultText = [resultText stringByAppendingString: error.description];
}
else{
resultText = response.responseText;
callback(#[[NSNull null], resultText]);
}
}];
}
#end
I have an app that I recently updated to work with in app purchases. The previous version (paid but with no in app purchases) was 1.0 and the current version is 1.1.
As the in app purchase essentially unlocks all features (which were included in the paid version 1.0), I wanted a way for users that had originally downloaded version 1.0 to be upgraded if they pressed my restore purchases button.
To do this, I first try to restore purchases and if the response to:
- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
If this provides me with a queue with a transactions count of 0, I check the receipt to see if the original version installed was 1.0.
The code to get the receipt is as per Apple's documentation
- (void)tryRestoreFromOriginalPurchase
{
// Load the receipt from the app bundle
NSError *error;
NSData *receipt = [NSData dataWithContentsOfURL:[[NSBundle mainBundle] appStoreReceiptURL]];
if (receipt == nil) {
[self restoreFromOriginalVersionWithReceipt:nil];
return;
}
// Create the JSON object that describes the request
NSDictionary *requestContents = #{#"receipt-data": [receipt base64EncodedStringWithOptions:0]};
NSData *requestData = [NSJSONSerialization dataWithJSONObject:requestContents options:0 error:&error];
if (!requestData) {
[self restoreFromOriginalVersionWithReceipt:nil];
return;
}
// Create a POST request with the receipt data
NSURL *storeURL = [NSURL URLWithString:#"https://buy.itunes.apple.com/verifyReceipt"];
NSMutableURLRequest *storeRequest = [NSMutableURLRequest requestWithURL:storeURL];
[storeRequest setHTTPMethod:#"POST"];
[storeRequest setHTTPBody:requestData];
// Make a connection to the iTunes Store on a background queue
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:storeRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (!connectionError) {
NSError *error;
NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
if (jsonResponse) [self restoreFromOriginalVersionWithReceipt:jsonResponse];
else [self restoreFromOriginalVersionWithReceipt:nil];
} else {
[self restoreFromOriginalVersionWithReceipt:nil];
}
}];
}
This then calls the following method:
- (void)restoreFromOriginalVersionWithReceipt:(NSDictionary *)receipt
{
if (receipt == nil) {
// CALL METHOD TO HANDLE FAILED RESTORE
} else {
NSInteger status = [[receipt valueForKey:#"status"] integerValue];
if (status == 0) {
NSString *originalApplicationVersion = [[receipt valueForKey:#"receipt"] valueForKey:#"original_application_version"];
if (originalApplicationVersion != nil && [originalApplicationVersion isEqualToString:#"1.0"]) {
// CALL METHOD TO HANDLE SUCCESSFUL RESTORE
} else {
// CALL METHOD TO HANDLE FAILED RESTORE
}
} else {
// CALL METHOD TO HANDLE FAILED RESTORE
}
}
}
Now this doesn't work. When someone installs version 1.1 and taps restore purchases, it restores successfully when it shouldn't.
I've just realized that in my Info.plist, my CFBundleShortVersionString is 1.1 but my CFBundleVersion is 1.0.
This may be a really daft question, but is the receipt providing an original_application_version of 1.0 (due to the wrong CFBundleVersion) even though the update is versioned 1.1?
So if I release a new update with this corrected to say version 1.2 (for both the CFBundleShortVersionString and CFBundleVersion), will the problem be resolved?
-- UPDATE --
So I just uploaded a new version to the app store with both the CGBundleVersion and CFBundleShortVersionString equal to 1.2. However, I'm still facing the same problem - users downloading version 1.2 for the first time and tapping on restore purchases are being upgraded for free (due to the receipt check outlined above). It seems the original_application_version is always coming to 1.0.
Note: I am downloading the app under a new iTunes account that has not previously downloaded the app.
Here is the receipt I have if I install from the app store and then try to get the receipt through Xcode
2014-08-27 08:46:42.858 AppName[4138:1803] {
environment = Production;
receipt = {
"adam_id" = AppID;
"application_version" = "1.0";
"bundle_id" = "com.CompanyName.AppName";
"download_id" = 94004873536255;
"in_app" = (
);
"original_application_version" = "1.0";
"original_purchase_date" = "2014-08-26 22:30:49 Etc/GMT";
"original_purchase_date_ms" = 1409092249000;
"original_purchase_date_pst" = "2014-08-26 15:30:49 America/Los_Angeles";
"receipt_type" = Production;
"request_date" = "2014-08-26 22:46:42 Etc/GMT";
"request_date_ms" = 1409093202544;
"request_date_pst" = "2014-08-26 15:46:42 America/Los_Angeles";
};
status = 0;
}
Any thoughts?
I stumbled upon the same issue - I converted my app from paid to freemium and tried to use original_application_version in the app receipt to decide who to unlock the new freemium features for. I, too, was unsuccessful.
However, what I found out was that I was using original_application_version incorrectly. The name misled me into thinking that this string corresponds to the version number of the app. On iOS, it does not. original_application_version is actually the build number of the app.
Original Application Version
This corresponds to the value of CFBundleVersion (in iOS) or
CFBundleShortVersionString (in macOS) in the Info.plist file when the
purchase was originally made. In the sandbox environment, the value of this field is always “1.0”.
Source: https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
I think this could be the reason why you are getting a number you are not expecting.
Using original_purchase_date, as you did in the end, is a reliable alternative though.
It's been a while since this question was asked, but it raises some very important points:
The original app version field in the receipt corresponds to CFBundleVersion, not CFBundleShortVersionString. In a sandbox (developer build) environment, the value string is always "1.0".
Note that when converting from a paid app to freemium, if an original (paid version) user uninstalls the app, then reinstalls from iTunes, there will be no local receipt. Calling SKPaymentQueue restoreCompletedTransactions will not cause a new receipt to be downloaded if that user has never made an in-app purchase. In this situation, you need ask for a receipt refresh using SKReceiptRefreshRequest and not rely on restore functionality.
Upgrading our Facebook iOS integration from the 2.x SDK to 3.x SDK automatically logs users out who were previously logged in, since the authentication credentials that we used to have to manually handle ourselves are now handled behind-the-scenes by the new SDK.
Is there any way to force the 3.x SDK to authenticate using the access token and expiration date that we were previously manually storing, as a one-time authentication migration?
Thanks in advance!
Finally figured it out. The solution involves using the FBSessionTokenCachingStrategy object they provide, and specifically an FBSessionManualTokenCachingStrategy:
if (isUserUpgrading) {
FBSessionTokenCachingStrategy *strategy = [[[FBSessionManualTokenCachingStrategy alloc] initWithUserDefaultTokenInformationKeyName:nil] autorelease];
strategy.accessToken = [[NSUserDefaults standardUserDefaults] stringForKey:#"FBSessionToken"]; // use your own UserDefaults key
strategy.expirationDate = [[NSUserDefaults standardUserDefaults] objectForKey:#"FBSessionExpiration"]; // use your own UserDefaults key
FBSession *session = [[[FBSession alloc] initWithAppID:#"MY_APP_ID" // use your own appId
permissions:nil
urlSchemeSuffix:nil
tokenCacheStrategy:strategy] autorelease];
[FBSession setActiveSession:session];
} else {
[FBSession openActiveSessionWithReadPermissions:...]; // normal authentication
}
For Facebook sdk 3.1.1 I had to create a new class FBSessionManualTokenCachingStrategy which is a subclass of FBSessionTokenCachingStrategy and defines a fetchTokenInformation method as
- (NSDictionary*)fetchTokenInformation; {
return [NSDictionary dictionaryWithObjectsAndKeys:
self.accessToken, FBTokenInformationTokenKey,
self.expirationDate, FBTokenInformationExpirationDateKey,
nil];
}
I am storing generic passwords in the keychain following Apple's example code in the "Keychain Services Programming Guide".
Everything works fine as long as I am running the App in Debug mode from Xcode. However when I archive and export the app, it will still store passwords (visible in Keychain Access) but is not able to retrieve them.
The keychain constantly returns errSecAuthFailed (-25293). This occurs on Mountain Lion but not on Snow Leopard. My App is code signed and sandboxed. To me it seems that when retrieving the password, keychain does not recognize the App as the same one that stored the password, because when I set the password to be accessible by any application it also works well.
I use the following code:
+ (NSString*) retrievePasswordFromKeychainWithKey: (NSString*) theKey {
SecKeychainUnlock(NULL, 0, NULL, FALSE);
const char* userNameUTF8 = [NSUserName() UTF8String];
uint32_t userNameLength = (uint32_t)strlen(userNameUTF8);
uint32_t serviceNameLength = (uint32_t)strlen([theKey UTF8String]);
uint32_t pwLength = 0;
void* pwBuffer = nil;
SecKeychainItemRef itemRef = nil;
OSStatus status1 = SecKeychainFindGenericPassword (NULL, serviceNameLength, serviceNameUTF8, userNameLength, userNameUTF8, &pwLength, &pwBuffer, &itemRef);
if (status1 == noErr) {
NSData* pwData = [NSData dataWithBytes:pwBuffer length:pwLength];
SecKeychainItemFreeContent (NULL, //No attribute data to release
pwBuffer //Release data buffer allocated by SecKeychainFindGenericPassword
);
return [NSString stringWithCString:[pwData bytes] encoding:NSUTF8StringEncoding];
}
//status1 is always -25293
return nil;
}
OK, I just learnt that this is an open bug in Mac OS 10.8.0. Apps signed with a Developer ID cannot access data from the keychain.
I hope this will be fixed in 10.8.1...
A workaround is not to sign the App with your Developer ID. (I have also read that Apps built under Lion are not affected by this bug, but I could not test this, yet)