Skip to content

Commit

Permalink
Added support for converting base64/canvas into Library PHAsset local…
Browse files Browse the repository at this point in the history
…identifier, to be then opened by IG.

This was to bypass Document Interaction API approach, which is clearly not working on some IOS 14+ devices for specific Instagram versions.

Further notes:
The error messages in terminal from xcode do not indicate any reason why DI would fail. And on latest 14.4.1 of IOS with latest instagram, the code which is broken works fine.
I believe it's caused by instagram itself failing to process the "Copy to" DI event, since the error is resolved on latest.

Therefore this commit simply introduces developer control to specific precisely which "logic mode" (approach) to use.
You can use legacy... IGO (exclusivegram), or 13+  IG (photo), and now --- LIBRARY mode which works as described above.
DEFAULT Mode is ... the default. And so existing user code will be left unaffected by this change. It will continue as if no change was made.
User can opt-in to use the new logistics, if they so choose.

REF: vstirbu#117
  • Loading branch information
ensemblebd committed Mar 23, 2021
1 parent 6dcddd5 commit 5581fa2
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 23 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,28 @@ You can get a LocalIdentifier by using Photos Framework [Fetching Assets](https:

A very basic application that uses the plugin can be found [here](https://github.com/vstirbu/instagramplugin-example).

Additionally for IOS only, you can utilize a fourth parameter on the base64/canvas method `share`, to correspond with a mode:
```
console.log(Instagram.SHARE_MODES);
> {
DEFAULT: 0, // original plugin logistics, where it checks for IOS Version 13+ to switch from IG to IGO mode.
IGO: 1, // legacy UTI for instagram DI (.exclusivegram)
IG: 2, // new UTI for instagram DI (.photo)
LIBRARY: 3 // save base64 or canvas to disk jpg, copy it to the Library, so that it can then be shared via App Intent
}
```
Using above as definition, you can change your code as such (note the usage of a blank caption, to specify 4 total arguments, not 3):
```
var caption = ''; // copied to clipboard by Cordova js. Instagram doesn't support feeding it anymore.
Instagram.share(canvasIdOrDataUrl, caption, function (err) {
if (err) {
console.log("not shared");
} else {
console.log("shared");
}
}, Instagram.SHARE_MODES.LIBRARY);
```

### AngularJS/Ionic

The plugin is included in [ngCordova](http://ngcordova.com/docs/plugins/instagram/) and [ionic-native](https://github.com/driftyco/ionic-native).
Expand Down
130 changes: 112 additions & 18 deletions src/ios/CDVInstagramPlugin.m
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,27 @@ a copy of this software and associated documentation files (the

#import <Cordova/CDV.h>
#import "CDVInstagramPlugin.h"
#import <Photos/Photos.h>

#define IOS_VERSION ([[[UIDevice currentDevice] systemVersion] floatValue])
#define IS_IOS13orHIGHER ([[[UIDevice currentDevice] systemVersion] floatValue] >= 13.0)
#define IS_IOS142orHIGHER ([[[UIDevice currentDevice] systemVersion] floatValue] >= 14.2)

static NSString *InstagramId = @"com.burbn.instagram";

typedef NS_ENUM(NSUInteger, LOGIC_MODE) {
LM_DEFAULT = 0,
LM_IGO,
LM_IG,
LM_LIBRARY
};
typedef NS_ENUM(NSUInteger, ERROR_CODE) {
EC_INSTAGRAM_INACCESSIBLE = 1,
EC_OTHER_APP_LAUNCHED,
EC_APP_INTENT_LAUNCH_FAILURE,
EC_APP_INTENT_GENERAL_FAILURE
};

@implementation CDVInstagramPlugin

@synthesize toInstagram;
Expand All @@ -39,6 +55,8 @@ -(void)isInstalled:(CDVInvokedUrlCommand*)command {
self.callbackId = command.callbackId;
CDVPluginResult *result;

NSLog(@"IOS Version: %f", IOS_VERSION);

NSURL *instagramURL = [NSURL URLWithString:@"instagram://app"];
if ([[UIApplication sharedApplication] canOpenURL:instagramURL]) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
Expand All @@ -55,41 +73,114 @@ - (void)share:(CDVInvokedUrlCommand*)command {
self.toInstagram = FALSE;
NSString *objectAtIndex0 = [command argumentAtIndex:0];
NSString *caption = [command argumentAtIndex:1];
NSNumber *mode = [command argumentAtIndex:2];

CDVPluginResult *result;
__block CDVPluginResult *result;

NSURL *instagramURL = [NSURL URLWithString:@"instagram://app"];
if ([[UIApplication sharedApplication] canOpenURL:instagramURL]) {
NSLog(@"open in instagram");

NSData *imageObj = [[NSData alloc] initWithBase64EncodedString:objectAtIndex0 options:0];
NSString *tmpDir = NSTemporaryDirectory();

NSString *path;
if (IS_IOS13orHIGHER) {
NSString *uti;

if (mode.intValue == LM_DEFAULT) {
if (IS_IOS13orHIGHER) {
path = [tmpDir stringByAppendingPathComponent:@"instagram.ig"];
uti = @"com.instagram.photo";
} else {
path = [tmpDir stringByAppendingPathComponent:@"instagram.igo"];
uti = @"com.instagram.exclusivegram";
}
NSLog(@"Using DEFAULT logic mode: %@", path);
}
else if (mode.intValue == LM_IG) {
path = [tmpDir stringByAppendingPathComponent:@"instagram.ig"];
} else {
uti = @"com.instagram.photo";
}
else if (mode.intValue == LM_IGO) {
path = [tmpDir stringByAppendingPathComponent:@"instagram.igo"];
uti = @"com.instagram.exclusivegram";
}
else {
NSString *fileName;
fileName = @"cordova-instagram.jpg"; // todo: perhaps a random hash would be better.
path = [tmpDir stringByAppendingPathComponent:fileName];
}

NSLog(@"Saving temporary file under app specific folder: %@", path);
[imageObj writeToFile:path atomically:true];

self.interactionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:path]];

if (IS_IOS13orHIGHER) {
self.interactionController .UTI = @"com.instagram.photo";
if (mode.intValue != LM_LIBRARY) {
NSLog(@"launching with Document Interaction UTI");
self.interactionController = [UIDocumentInteractionController interactionControllerWithURL:[NSURL fileURLWithPath:path]];

// not sure why this is here. It doesn't work and is pointless. Posterity?
if (caption) {
self.interactionController .annotation = @{@"InstagramCaption" : caption};
}

self.interactionController .UTI = uti;
self.interactionController .delegate = self;
if ([self.interactionController presentOpenInMenuFromRect:CGRectZero inView:self.webView animated:YES]){
NSLog(@"menu is presented");
}
} else {
self.interactionController .UTI = @"com.instagram.exclusivegram";
}
NSLog(@"Attempting to save to library, read as a PHAsset for it's localidentifier, and launch using App Intent.");
UIImage *image = [UIImage imageWithContentsOfFile:path];

__block NSString* localId;

// Add it to the photo library
NSLog(@"Sharing to library now..");
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
PHAssetChangeRequest *assetChangeRequest = [PHAssetChangeRequest creationRequestForAssetFromImage:image];
localId = [[assetChangeRequest placeholderForCreatedAsset] localIdentifier];
} completionHandler:^(BOOL success, NSError *error) {
if (!success) {
NSLog(@"Error creating asset: %@", error);
} else {
@try {
NSString *localIdentifierEscaped = [localId stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]];
NSURL *instagramShareURL = [NSURL URLWithString:[NSString stringWithFormat:@"instagram://library?LocalIdentifier=%@", localIdentifierEscaped]];
NSLog(@"Opening %@, using intent: %@", localId, instagramShareURL);

if (caption) {
self.interactionController .annotation = @{@"InstagramCaption" : caption};
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] openURL:instagramShareURL options:@{} completionHandler:^(BOOL success) {
if (success) {
NSLog(@"Successfully opened instagram");

dispatch_async(dispatch_get_main_queue(), ^{
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId: self.callbackId];
});
}
else {
NSLog(@"Failed to open instagram");

dispatch_async(dispatch_get_main_queue(), ^{
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:EC_APP_INTENT_LAUNCH_FAILURE];
[self.commandDelegate sendPluginResult:result callbackId: self.callbackId];
});
}
}];
});
}
@catch(id anException) {
NSLog(@"Failed to open due to: %@", anException);
dispatch_async(dispatch_get_main_queue(), ^{
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:EC_APP_INTENT_GENERAL_FAILURE];
[self.commandDelegate sendPluginResult:result callbackId: self.callbackId];
});
}
}
}];

}
self.interactionController .delegate = self;
[self.interactionController presentOpenInMenuFromRect:CGRectZero inView:self.webView animated:YES];

} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:1];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:EC_INSTAGRAM_INACCESSIBLE];
[self.commandDelegate sendPluginResult:result callbackId: self.callbackId];
}
}
Expand All @@ -113,25 +204,28 @@ - (void)shareAsset:(CDVInvokedUrlCommand*)command {
[self.commandDelegate sendPluginResult:result callbackId: self.callbackId];

} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:1];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:EC_INSTAGRAM_INACCESSIBLE];
[self.commandDelegate sendPluginResult:result callbackId: self.callbackId];
}
}

- (void) documentInteractionController: (UIDocumentInteractionController *) controller willBeginSendingToApplication: (NSString *) application {
NSLog(@"willBeginSendingToApplication");
if ([application isEqualToString:InstagramId]) {
self.toInstagram = TRUE;
}
}

- (void) documentInteractionControllerDidDismissOpenInMenu: (UIDocumentInteractionController *) controller {
CDVPluginResult *result;

NSLog(@"documentInteractionControllerDidDismissOpenInMenu");

if (self.toInstagram) {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK];
[self.commandDelegate sendPluginResult:result callbackId: self.callbackId];
} else {
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:2];
result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageToErrorObject:EC_OTHER_APP_LAUNCHED];
[self.commandDelegate sendPluginResult:result callbackId: self.callbackId];
}
}
Expand Down
23 changes: 18 additions & 5 deletions www/CDVInstagramPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var exec = require('cordova/exec');
var hasCheckedInstall,
isAppInstalled;

function shareDataUrl(dataUrl, caption, callback) {
function shareDataUrl(dataUrl, caption, callback, mode) {
var imageData = dataUrl.replace(/data:image\/(png|jpeg);base64,/, "");

if (cordova && cordova.plugins && cordova.plugins.clipboard && caption !== '') {
Expand All @@ -41,11 +41,17 @@ function shareDataUrl(dataUrl, caption, callback) {
},
function (err) {
callback && callback(err);
}, "Instagram", "share", [imageData, caption]
}, "Instagram", "share", [imageData, caption, mode]
);
}

var Plugin = {
SHARE_MODES: {
DEFAULT: 0,
IGO: 1,
IG: 2,
LIBRARY: 3
},
// calls to see if the device has the Instagram app
isInstalled: function (callback) {
exec(function (version) {
Expand All @@ -63,7 +69,8 @@ var Plugin = {
share: function () {
var data,
caption,
callback;
callback,
mode = this.SHARE_MODES.DEFAULT; // existing code will continue to function as normal. But users can "opt in" to use mode 1,2 or 3.

switch(arguments.length) {
case 2:
Expand All @@ -76,6 +83,12 @@ var Plugin = {
caption = arguments[1];
callback = arguments[2];
break;
case 4:
data = arguments[0];
caption = arguments[1];
callback = arguments[2];
mode = arguments[3];
break;
default:
}

Expand All @@ -89,10 +102,10 @@ var Plugin = {
magic = "data:image";

if (canvas) {
shareDataUrl(canvas.toDataURL(), caption, callback);
shareDataUrl(canvas.toDataURL(), caption, callback, mode);
}
else if (data.slice(0, magic.length) == magic) {
shareDataUrl(data, caption, callback);
shareDataUrl(data, caption, callback, mode);
}
else
{
Expand Down

0 comments on commit 5581fa2

Please sign in to comment.