From 530cdef5d94c4ba95776d50606143c6d304e5e86 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen Date: Fri, 24 Apr 2020 00:57:48 +0700 Subject: [PATCH] feat: Implement Facebook stories share (#761) * feat: Implement Facebook stories share * Update index.js Co-Authored-By: Mateus Andrade * Update README.md Co-Authored-By: Mateus Andrade * Update README.md Co-Authored-By: Mateus Andrade Co-authored-by: Mateus Andrade --- README.md | 76 +++++++++--- index.js | 9 ++ ios/FacebookStories.h | 54 +++++++++ ios/FacebookStories.m | 163 ++++++++++++++++++++++++++ ios/RNShare.m | 12 ++ ios/RNShare.xcodeproj/project.pbxproj | 6 + 6 files changed, 302 insertions(+), 18 deletions(-) create mode 100644 ios/FacebookStories.h create mode 100644 ios/FacebookStories.m diff --git a/README.md b/README.md index 609d1b2d..21f093b0 100644 --- a/README.md +++ b/README.md @@ -17,12 +17,12 @@ This gives you the power to prioritize our work and support the project contribu ## Automatic Way --- -``` +``` yarn add react-native-share ``` or if you're using npm -``` +``` npm install react-native-share --save ``` --- @@ -35,7 +35,7 @@ npm install react-native-share --save #### Important: Linking is not needed anymore. ``react-native@0.60.0+`` supports dependencies auto linking. For iOS you also need additional step to install auto linked Pods (Cocoapods should be installed): -``` +``` cd ios && pod install && cd ../ ``` ___ @@ -47,28 +47,28 @@ After installing jetifier, runs a ```npx jetify -r``` and test if this works by ## Automatic Way --- -``` +``` yarn add react-native-share react-native link react-native-share # not needed for react-native >= 0.60.0 ``` or if you're using npm -``` +``` npm install react-native-share --save react-native link react-native-share # not needed for react-native >= 0.60.0 ``` --- -We recommend using the releases from npm, however you can use the master branch if you need any feature that is not available on NPM. By doing this you will be able to use unreleased features, but the module may be less stable. -**yarn**: -``` +We recommend using the releases from npm, however you can use the master branch if you need any feature that is not available on NPM. By doing this you will be able to use unreleased features, but the module may be less stable. +**yarn**: +``` yarn add react-native-share@git+https://git@github.com/react-native-community/react-native-share.git ``` --- #### LSApplicationQueriesSchemes on iOS Remember to add `instagram`, `facebook` or whatever queries schemes you need to LSApplicationQueriesSchemes -field in your Info.plist. This is required to share content directly to other apps like Instagram, Facebook etc. +field in your Info.plist. This is required to share content directly to other apps like Instagram, Facebook etc. Values for queries schemes can be found in `Social` field of `RNShare` class. @@ -84,7 +84,7 @@ Values for queries schemes can be found in `Social` field of `RNShare` class. 4. In XCode, in the project navigator, select your project. Add `libRNShare.a` to your project's `Build Phases` ➜ `Link Binary With Libraries` 5. In XCode, in the project navigator, select your project. Add `Social.framework` and `MessageUI.framework` to your project's `General` ➜ `Linked Frameworks and Libraries` 6. In iOS 9 or higher, you should add app list that you will share. -If you want to share Whatsapp and Mailto, you should write `LSApplicationQueriesSchemes` in info.plist +If you want to share Whatsapp and Mailto, you should write `LSApplicationQueriesSchemes` in info.plist ```xml LSApplicationQueriesSchemes @@ -111,7 +111,7 @@ You just need to add to your Podfile the react-native-share dependency. pod 'RNShare', :path => '../node_modules/react-native-share' ``` -After that, just run a `pod install` or `pod udpate` to get up and running with react-native-share. +After that, just run a `pod install` or `pod udpate` to get up and running with react-native-share. Then run a `react-native link react-native-share`, and doing the steps 6 and 7. @@ -351,7 +351,7 @@ Supported options: --- ### isPackageInstalled() (in Android) -It's a method that checks if an app (package) is installed on Android. +It's a method that checks if an app (package) is installed on Android. It returns a promise with `isInstalled`. e.g. Checking if Instagram is installed on Android. @@ -376,7 +376,7 @@ const shareOptions = { url: 'some share url', social: Share.Social.WHATSAPP, whatsAppNumber: "9199999999", // country code + phone number - filename: 'test' , // only for base64 file in Android + filename: 'test' , // only for base64 file in Android }; Share.shareSingle(shareOptions); ``` @@ -384,6 +384,7 @@ Share.shareSingle(shareOptions); | Name | Android | iOS | Windows | | :---- | :------: | :--- | :--- | **FACEBOOK** | yes | yes | no | +| **FACEBOOK_STORIES** | no | yes | no | | **PAGESMANAGER** | yes | no | no | | **WHATSAPP** | yes | yes | no | | **INSTAGRAM** | yes | yes | no | @@ -526,10 +527,49 @@ Supported options for INSTAGRAM_STORIES: | **SHARE_STICKER_IMAGE** | stickerImage | | **SHARE_BACKGROUND_AND_STICKER_IMAGE** | backgroundImage, stickerImage | +### Static Values for Facebook Stories +These can be assessed using Share.Social property +For eg. +```javascript +import Share from 'react-native-share'; + +const shareOptions = { + method: Share.FacebookStories.SHARE_BACKGROUND_AND_STICKER_IMAGE, + backgroundImage: 'http://urlto.png', // url or an base64 string + stickerImage: 'data:image/png;base64,', //or you can use "data:" url + backgroundBottomColor: '#fefefe', + backgroundTopColor: '#906df4', + attributionURL: 'http://deep-link-to-app', //in beta + appId: '219376304', //facebook appId + social: Share.Social.FACEBOOK_STORIES +}; +Share.shareSingle(shareOptions); +``` + +Supported options for FACEBOOK_STORIES: + +| Name | Type | Description | +| :---- | :------: | :--- | +| appId | string | (required) facebook appId | +| backgroundImage | string | URL you want to share | +| stickerImage | string | URL you want to share | +| method | string | [List](#instagram-stories-method-list) | +| backgroundBottomColor | string | (optional) default #837DF4 | +| backgroundTopColor | string | (optional) default #906df4 | +| attributionURL | string | (optional) facebook beta-test | + +### Facebook stories method list +| Name | Required options | +| :---- | :------: | +| **SHARE_BACKGROUND_IMAGE** | backgroundImage | +| **SHARE_STICKER_IMAGE** | stickerImage | +| **SHARE_BACKGROUND_AND_STICKER_IMAGE** | backgroundImage, stickerImage | + + #### Adding your implementation of FileProvider [Android guide](https://developer.android.com/training/secure-file-sharing/setup-sharing.html). - + - `applicationId` should be defined in the `defaultConfig` section in your `android/app/build.gradle`: - File: `android/app/build.gradle` @@ -540,7 +580,7 @@ Supported options for INSTAGRAM_STORIES: ... } ``` - + - Add this `` section to your `AndroidManifest.xml` File: `AndroidManifest.xml` @@ -559,10 +599,10 @@ Supported options for INSTAGRAM_STORIES: ``` - Create a `filepaths.xml` under this directory: -`android/app/src/main/res/xml`. +`android/app/src/main/res/xml`. In this file, add the following contents: - + File: `android/app/src/main/res/xml/filepaths.xml` ```xml @@ -578,7 +618,7 @@ Supported options for INSTAGRAM_STORIES: ```java import cl.json.ShareApplication public class MainApplication extends Application implements ShareApplication, ReactApplication { - + @Override public String getFileProviderAuthority() { return BuildConfig.APPLICATION_ID + ".provider"; diff --git a/index.js b/index.js index dae43616..f5ba28a5 100644 --- a/index.js +++ b/index.js @@ -92,6 +92,7 @@ type Options = { failOnCancel?: boolean, showAppsToView?: boolean, saveToFiles?: boolean, + appId: string, }; type MultipleOptions = { url?: string, @@ -201,6 +202,7 @@ class RNShare { static Sheet: any; static Social = { FACEBOOK: NativeModules.RNShare.FACEBOOK || 'facebook', + FACEBOOK_STORIES: NativeModules.RNShare.FACEBOOK_STORIES || 'facebook-stories', PAGESMANAGER: NativeModules.RNShare.PAGESMANAGER || 'pagesmanager', TWITTER: NativeModules.RNShare.TWITTER || 'twitter', WHATSAPP: NativeModules.RNShare.WHATSAPP || 'whatsapp', @@ -219,6 +221,13 @@ class RNShare { NativeModules.RNShare.SHARE_BACKGROUND_AND_STICKER_IMAGE || 'shareBackgroundAndStickerImage', }; + static FacebookStories = { + SHARE_BACKGROUND_IMAGE: NativeModules.RNShare.SHARE_BACKGROUND_IMAGE || 'shareBackgroundImage', + SHARE_STICKER_IMAGE: NativeModules.RNShare.SHARE_STICKER_IMAGE || 'shareStickerImage', + SHARE_BACKGROUND_AND_STICKER_IMAGE: + NativeModules.RNShare.SHARE_BACKGROUND_AND_STICKER_IMAGE || 'shareBackgroundAndStickerImage', + }; + static open(options: Options | MultipleOptions): Promise { return new Promise((resolve, reject) => { requireAndAskPermissions(options) diff --git a/ios/FacebookStories.h b/ios/FacebookStories.h new file mode 100644 index 00000000..b39a3f1e --- /dev/null +++ b/ios/FacebookStories.h @@ -0,0 +1,54 @@ +// +// FacebookStories.h +// RNShare +// +// Created by Quynh Nguyen on 4/13/20. +// Link: https://github.com/Quynh-Nguyen +// Copyright © 2020 Facebook. All rights reserved. +// + +#import +// import RCTConvert +#if __has_include() +#import +#elif __has_include("RCTConvert.h") +#import "RCTConvert.h" +#else +#import "React/RCTConvert.h" // Required when used as a Pod in a Swift project +#endif +// import RCTBridge +#if __has_include() +#import +#elif __has_include("RCTBridge.h") +#import "RCTBridge.h" +#else +#import "React/RCTBridge.h" // Required when used as a Pod in a Swift project +#endif +// import RCTUIManager +#if __has_include() +#import +#elif __has_include("RCTUIManager.h") +#import "RCTUIManager.h" +#else +#import "React/RCTUIManager.h" // Required when used as a Pod in a Swift project +#endif +// import RCTLog +#if __has_include() +#import +#elif __has_include("RCTLog.h") +#import "RCTLog.h" +#else +#import "React/RCTLog.h" // Required when used as a Pod in a Swift project +#endif +// import RCTUtils +#if __has_include() +#import +#elif __has_include("RCTUtils.h") +#import "RCTUtils.h" +#else +#import "React/RCTUtils.h" // Required when used as a Pod in a Swift project +#endif +@interface FacebookStories : NSObject + +- (void *) shareSingle:(NSDictionary *)options failureCallback:(RCTResponseErrorBlock)failureCallback successCallback:(RCTResponseSenderBlock)successCallback; +@end diff --git a/ios/FacebookStories.m b/ios/FacebookStories.m new file mode 100644 index 00000000..4a9fe4cb --- /dev/null +++ b/ios/FacebookStories.m @@ -0,0 +1,163 @@ +// +// FacebookStories.m +// RNShare +// +// Created by Quynh Nguyen on 4/13/20. +// Link: https://github.com/Quynh-Nguyen +// Copyright © 2020 Facebook. All rights reserved. +// + +// import RCTLog +#if __has_include() +#import +#elif __has_include("RCTLog.h") +#import "RCTLog.h" +#else +#import "React/RCTLog.h" // Required when used as a Pod in a Swift project +#endif + +#import "FacebookStories.h" + +@implementation FacebookStories +RCT_EXPORT_MODULE(); + +- (void)backgroundImage:(NSData *)backgroundImage attributionURL:(NSString *)attributionURL appId:(NSString *)appId { + // Verify app can open custom URL scheme, open if able + + NSURL *urlScheme = [NSURL URLWithString:@"facebook-stories://share"]; + if ([[UIApplication sharedApplication] canOpenURL:urlScheme]) { + // Assign background image asset and attribution link URL to pasteboard + NSArray *pasteboardItems = @[@{@"com.facebook.sharedSticker.backgroundImage" : backgroundImage, @"com.facebook.sharedSticker.contentURL" : attributionURL, @"com.facebook.sharedSticker.appID" : appId}]; + NSDictionary *pasteboardOptions = @{UIPasteboardOptionExpirationDate : [[NSDate date] dateByAddingTimeInterval:60 * 5]}; + // This call is iOS 10+, can use 'setItems' depending on what versions you support + [[UIPasteboard generalPasteboard] setItems:pasteboardItems options:pasteboardOptions]; + [[UIApplication sharedApplication] openURL:urlScheme options:@{} completionHandler:nil]; + } else { // Handle older app versions or app not installed case + [self fallbackFacebook]; + } +} + +- (void)stickerImage:(NSData *)stickerImage + backgroundTopColor:(NSString *)backgroundTopColor +backgroundBottomColor:(NSString *)backgroundBottomColor + attributionURL:(NSString *)attributionURL + appId:(NSString *)appId +{ + // Verify app can open custom URL scheme. If able, + // assign assets to pasteboard, open scheme. + + NSURL *urlScheme = [NSURL URLWithString:@"facebook-stories://share"]; + if ([[UIApplication sharedApplication] canOpenURL:urlScheme]) { + + // Assign sticker image asset and attribution link URL to pasteboard + + NSArray *pasteboardItems = @[@{@"com.facebook.sharedSticker.stickerImage" : stickerImage, @"com.facebook.sharedSticker.backgroundTopColor" : backgroundTopColor, @"com.facebook.sharedSticker.backgroundBottomColor" : backgroundBottomColor, @"com.facebook.sharedSticker.contentURL" : attributionURL, @"com.facebook.sharedSticker.appID" : appId}]; + + NSDictionary *pasteboardOptions = @{UIPasteboardOptionExpirationDate : [[NSDate date] dateByAddingTimeInterval:60 * 5]}; + + // This call is iOS 10+, can use 'setItems' depending on what versions you support + + [[UIPasteboard generalPasteboard] setItems:pasteboardItems options:pasteboardOptions]; + [[UIApplication sharedApplication] openURL:urlScheme options:@{} completionHandler:nil]; + + } else { // Handle older app versions or app not installed case + [self fallbackFacebook]; + } +} + +- (void)backgroundImage:(NSData *)backgroundImage stickerImage:(NSData *)stickerImage attributionURL:(NSString *)attributionURL appId:(NSString *)appId +{ + // Verify app can open custom URL scheme. If able, + // assign assets to pasteboard, open scheme. + NSURL *urlScheme = [NSURL URLWithString:@"facebook-stories://share"]; + if ([[UIApplication sharedApplication] canOpenURL:urlScheme]) { + // Assign background and sticker image assets and + // attribution link URL to pasteboard + NSArray *pasteboardItems = @[@{@"com.facebook.sharedSticker.backgroundImage" : backgroundImage, @"com.facebook.sharedSticker.stickerImage" : stickerImage, @"com.facebook.sharedSticker.contentURL" : attributionURL, @"com.facebook.sharedSticker.appID" : appId}]; + NSDictionary *pasteboardOptions = @{UIPasteboardOptionExpirationDate : [[NSDate date] dateByAddingTimeInterval:60 * 5]}; + // This call is iOS 10+, can use 'setItems' depending on what versions you support + [[UIPasteboard generalPasteboard] setItems:pasteboardItems options:pasteboardOptions]; + [[UIApplication sharedApplication] openURL:urlScheme options:@{} completionHandler:nil]; + + } else { // Handle older app versions or app not installed case + [self fallbackFacebook]; + } +} + + +- (void)shareSingle:(NSDictionary *)options + failureCallback:(RCTResponseErrorBlock)failureCallback + successCallback:(RCTResponseSenderBlock)successCallback { + + NSString *attrURL = [RCTConvert NSString:options[@"attributionURL"]]; + if (attrURL == nil) { + attrURL = @""; + } + + NSString *appId = [RCTConvert NSString:options[@"appId"]]; + + NSString *method = [RCTConvert NSString:options[@"method"]]; + if (method) { + if([method isEqualToString:@"shareBackgroundImage"]) { + + NSURL *URL = [RCTConvert NSURL:options[@"backgroundImage"]]; + if (URL == nil) { + RCTLogError(@"key 'backgroundImage' missing in options"); + } else { + UIImage *image = [UIImage imageWithData: [NSData dataWithContentsOfURL:URL]]; + + [self backgroundImage:UIImagePNGRepresentation(image) attributionURL:attrURL appId:appId]; + } + } else if([method isEqualToString:@"shareStickerImage"]) { + RCTLog(@"method shareStickerImage"); + + NSString *backgroundTopColor = [RCTConvert NSString:options[@"backgroundTopColor"]]; + if (backgroundTopColor == nil) { + backgroundTopColor = @"#906df4"; + } + NSString *backgroundBottomColor = [RCTConvert NSString:options[@"backgroundBottomColor"]]; + if (backgroundBottomColor == nil) { + backgroundBottomColor = @"#837DF4"; + } + + NSURL *URL = [RCTConvert NSURL:options[@"stickerImage"]]; + if (URL == nil) { + RCTLogError(@"key 'stickerImage' missing in options"); + } else { + UIImage *image = [UIImage imageWithData: [NSData dataWithContentsOfURL:URL]]; + + [self stickerImage:UIImagePNGRepresentation(image) backgroundTopColor:backgroundTopColor backgroundBottomColor:backgroundBottomColor attributionURL:attrURL appId:appId]; + } + } else if([method isEqualToString:@"shareBackgroundAndStickerImage"]) { + RCTLog(@"method shareBackgroundAndStickerImage"); + + NSURL *backgroundURL = [RCTConvert NSURL:options[@"backgroundImage"]]; + NSURL *sticketURL = [RCTConvert NSURL:options[@"stickerImage"]]; + + if (backgroundURL == nil || sticketURL == nil) { + RCTLogError(@"key 'backgroundImage' or 'stickerImage' missing in options"); + } else { + UIImage *backgroundImage = [UIImage imageWithData: [NSData dataWithContentsOfURL:backgroundURL]]; + UIImage *stickerImage = [UIImage imageWithData: [NSData dataWithContentsOfURL:sticketURL]]; + + [self backgroundImage:UIImagePNGRepresentation(backgroundImage) stickerImage:UIImagePNGRepresentation(stickerImage) attributionURL:attrURL appId:appId]; + } + } + } else { + RCTLogError(@"key 'method' missing in options"); + } +} + +- (void)fallbackFacebook { + // Cannot open facebook + NSString *stringURL = @"http://itunes.apple.com/app/facebook/id284882215"; + NSURL *url = [NSURL URLWithString:stringURL]; + [[UIApplication sharedApplication] openURL:url]; + + NSString *errorMessage = @"Not installed"; + NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedString(errorMessage, nil)}; + NSError *error = [NSError errorWithDomain:@"com.rnshare" code:1 userInfo:userInfo]; + + NSLog(errorMessage); +} +@end diff --git a/ios/RNShare.m b/ios/RNShare.m index 40e35768..d3c0022d 100644 --- a/ios/RNShare.m +++ b/ios/RNShare.m @@ -44,6 +44,7 @@ #import "WhatsAppShare.h" #import "InstagramShare.h" #import "InstagramStories.h" +#import "FacebookStories.h" #import "GooglePlusShare.h" #import "EmailShare.h" #import "RNShareActivityItemSource.h" @@ -89,6 +90,7 @@ - (NSDictionary *)constantsToExport { return @{ @"FACEBOOK": @"facebook", + @"FACEBOOK_STORIES": @"facebook-stories", @"TWITTER": @"twitter", @"GOOGLEPLUS": @"googleplus", @"WHATSAPP": @"whatsapp", @@ -113,6 +115,16 @@ - (NSDictionary *)constantsToExport NSLog(@"TRY OPEN FACEBOOK"); GenericShare *shareCtl = [[GenericShare alloc] init]; [shareCtl shareSingle:options failureCallback: failureCallback successCallback: successCallback serviceType: SLServiceTypeFacebook inAppBaseUrl:@"fb://"]; + } else if([social isEqualToString:@"facebook-stories"]) { + NSString *appId = [RCTConvert NSString:options[@"appId"]]; + if (appId) { + NSLog(@"TRY OPEN FACEBOOK STORIES"); + FacebookStories *shareCtl = [[FacebookStories alloc] init]; + [shareCtl shareSingle:options failureCallback: failureCallback successCallback: successCallback]; + } else { + RCTLogError(@"key 'appId' missing in options"); + return; + } } else if([social isEqualToString:@"twitter"]) { NSLog(@"TRY OPEN Twitter"); GenericShare *shareCtl = [[GenericShare alloc] init]; diff --git a/ios/RNShare.xcodeproj/project.pbxproj b/ios/RNShare.xcodeproj/project.pbxproj index cd2a0d24..f685cba6 100644 --- a/ios/RNShare.xcodeproj/project.pbxproj +++ b/ios/RNShare.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ B3E7B58A1CC2AC0600A0062D /* RNShare.m in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* RNShare.m */; }; B3EEE48E1D4844E7008422B6 /* GooglePlusShare.m in Sources */ = {isa = PBXBuildFile; fileRef = B3EEE48D1D4844E7008422B6 /* GooglePlusShare.m */; }; CE0C76371E9E7DE100ED396E /* InstagramShare.m in Sources */ = {isa = PBXBuildFile; fileRef = CE0C76361E9E7DE100ED396E /* InstagramShare.m */; }; + F1A1AD582444B09B00E32E33 /* FacebookStories.m in Sources */ = {isa = PBXBuildFile; fileRef = F1A1AD572444B09B00E32E33 /* FacebookStories.m */; }; FCC7BB4F221457EF00E1EA39 /* InstagramStories.m in Sources */ = {isa = PBXBuildFile; fileRef = FCC7BB4E221457EF00E1EA39 /* InstagramStories.m */; }; /* End PBXBuildFile section */ @@ -45,6 +46,8 @@ B3EEE48F1D4844F4008422B6 /* GooglePlusShare.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GooglePlusShare.h; sourceTree = ""; }; CE0C76361E9E7DE100ED396E /* InstagramShare.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InstagramShare.m; sourceTree = ""; }; CE0C76621E9E869300ED396E /* InstagramShare.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InstagramShare.h; sourceTree = ""; }; + F1A1AD562444B09B00E32E33 /* FacebookStories.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FacebookStories.h; sourceTree = ""; }; + F1A1AD572444B09B00E32E33 /* FacebookStories.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FacebookStories.m; sourceTree = ""; }; FCC7BB4D2214566C00E1EA39 /* InstagramStories.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = InstagramStories.h; sourceTree = ""; }; FCC7BB4E221457EF00E1EA39 /* InstagramStories.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InstagramStories.m; sourceTree = ""; }; /* End PBXFileReference section */ @@ -95,6 +98,8 @@ B3EEE48F1D4844F4008422B6 /* GooglePlusShare.h */, FCC7BB4D2214566C00E1EA39 /* InstagramStories.h */, FCC7BB4E221457EF00E1EA39 /* InstagramStories.m */, + F1A1AD562444B09B00E32E33 /* FacebookStories.h */, + F1A1AD572444B09B00E32E33 /* FacebookStories.m */, ); name = social; sourceTree = ""; @@ -156,6 +161,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + F1A1AD582444B09B00E32E33 /* FacebookStories.m in Sources */, 6D697F8423A86EBC00F754A1 /* RNShareActivityItemSource.m in Sources */, CE0C76371E9E7DE100ED396E /* InstagramShare.m in Sources */, B3EEE48E1D4844E7008422B6 /* GooglePlusShare.m in Sources */,