Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS][React][Feature] Add support for NSDictionary in the Pasteboard #646

Open
EvanBacon opened this issue Jun 18, 2020 · 2 comments
Open
Assignees
Labels
iOS iOS issue offtopic This issue is offtopic. Might be closed soon.

Comments

@EvanBacon
Copy link

EvanBacon commented Jun 18, 2020

I work on Expo and React, I was trying to get WhatsApp stickers working using the exposed native modules provided by react-native and the Expo SDK. This is in contrast to using specific native code dedicated to WhatsApp.

On iOS, nearly everything works except the interface with the Pasteboard.
react-native only surfaces the bare minimum for setting a string to the clipboard here.
Adding support for [UIPasteboard setItems] would be a reasonable native change to make, but even that wouldn't be enough due to the usage of NSJSONSerialization which makes things much trickier.

Here is the minimum required native Objective-C code for supporting stickers on iOS:

UM_EXPORT_METHOD_AS(copy, copy:(NSDictionary *)data resolver:(UMPromiseResolveBlock)resolve rejecter:(UMPromiseRejectBlock)reject)
{
    UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
    
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:data options:kNilOptions error:nil];
    if (@available(iOS 10.0.0, *)) {
        [pasteboard setItems:@[@{
                                   @"net.whatsapp.third-party.sticker-pack": jsonData
        }] options:@{}];
    }
    resolve(NSNull.null);
}

It's concise but also very WhatsApp specific. A more ideal interface would be something like this:

UM_EXPORT_METHOD_AS(copy, copy:(NSArray<NSDictionary *> *)data resolver:(UMPromiseResolveBlock)resolve rejecter:(UMPromiseRejectBlock)reject)
{
    UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];

    if (@available(iOS 10.0.0, *)) {
        [pasteboard setItems:data options:@{}];
    }
    resolve(NSNull.null);
}

On the Javascript side it can be used like this:

import { NativeModulesProxy } from '@unimodules/core';

NativeModulesProxy.Clipboard.copy({ 
  'net.whatsapp.third-party.sticker-pack': { /* Object created using other JS modules */ }
})

From what I understand, it may require changes on the WhatsApp iOS side to also accept an NSDictionary object in addition to the usual NSData generated by NSJSONSerialization.

If this is a reasonable change to make that would be awesome! Stickers would be really easy to create and customize using a non-native interface.

Further

I figured I'd also leave my findings here for anyone else trying to get this working in the future:

import { NativeModulesProxy, Platform } from "@unimodules/core";
import * as Application from "expo-application";
import { Asset } from "expo-asset";
import * as FileSystem from "expo-file-system";
import * as IntentLauncher from "expo-intent-launcher";
import * as Linking from "expo-linking";
import Constants from "expo-constants";

const appUrl = "whatsapp://stickerPack";

/**
 * Requires the following in your app.config.js:
 * "infoPlist": {
 *   "LSApplicationQueriesSchemes": ["whatsapp"]
 * }
 * This can be tested in bare-workflow or a custom client with `expo client:ios`
 */
export async function isAvailableAsync(): Promise<boolean> {
  // https://github.com/WhatsApp/stickers/blob/master/iOS/README.md#structure-of-the-json-file-that-is-sent-to-whatsapp
  return Linking.canOpenURL(appUrl);
}

function getAndroidReactContextPackageName(): string {
  return Application.applicationId;
}

async function sendToWhatsapp(json: Record<string, any>): Promise<boolean> {
  if (Platform.OS === "android") {
    await IntentLauncher.startActivityAsync(
      "com.whatsapp.intent.action.ENABLE_STICKER_PACK",
      {
        extra: {
          sticker_pack_id: json.identifier,
          sticker_pack_name: json.name,
          // https://github.com/WhatsApp/stickers/tree/master/Android#intent
          // todo: is there an alternative to a native content provider?
          sticker_pack_authority:
            getAndroidReactContextPackageName() + ".stickercontentprovider",
        },
      }
    );
    return true;
  }

  await NativeModulesProxy.Clipboard.setItems(
    [{ "net.whatsapp.third-party.sticker-pack": json }],
    {}
  );
  await Linking.openURL(appUrl);
  return true;
}

async function readFileAsB64(fileUri: string): Promise<string> {
  return FileSystem.readAsStringAsync(fileUri, {
    encoding: FileSystem.EncodingType.Base64,
  });
}

// Spec https://github.com/WhatsApp/stickers/blob/master/iOS/README.md
export async function send(config: {
  /**
   * The identifier should be unique and can be alphanumeric: a-z, A-Z, 0-9, and the following characters are also allowed "_", "-", "." and " ". The identifier should be less than 128 characters.
   */
  identifier: string;
  /**
   * the sticker pack's name (128 characters max)
   */
  name: string;
  publisher: string;
  stickers: { asset: number; emojis?: string[] }[];
  trayImage: number;
  /**
   * an overall representation of the version of the stickers and tray icon. When you update stickers or tray icon in your pack, please update this string, this will tell WhatsApp that the pack has new content and update the stickers on WhatsApp side.
   */
  image_data_version: string;
  /**
   * this tells WhatsApp that the stickers from your pack should not be cached. By default, you should keep it false. Exception is that if your app updates stickers without user actions, you can keep it true, for example: your app provides clock sticker that updates stickers every minute.
   */
  avoid_cache: boolean;

  // todo: publisher_website, privacy_policy_website, license_agreement_website
}) {
  const json: Record<string, any> = {
    identifier: config.identifier,
    name: config.name,
    publisher: config.publisher,
    // android
    // image_data_version: config.image_data_version,
    // avoid_cache: config.avoid_cache,
  };
  // todo: use image-manipulator to ensure PNG
  const asset = Asset.fromModule(config.trayImage);
  await asset.downloadAsync();
  json.tray_image = await readFileAsB64(asset.localUri);

  const stickersArray: Record<string, any>[] = [];
  for (const sticker of config.stickers) {
    const asset = Asset.fromModule(sticker.asset);
    await asset.downloadAsync();
    const b64webp = await readFileAsB64(asset.localUri);
    stickersArray.push({
      image_data: b64webp,
      emojis: sticker.emojis,
    });
  }
  json.stickers = stickersArray;

  json["ios_app_store_link"] = Constants.manifest?.ios?.appStoreUrl;
  json["android_play_store_link"] = Constants.manifest?.android?.playStoreUrl;

  return sendToWhatsapp(json);
}
@EvanBacon EvanBacon added the iOS iOS issue label Jun 18, 2020
@Zandor300 Zandor300 added the offtopic This issue is offtopic. Might be closed soon. label Jun 20, 2020
@emiliocortina
Copy link

Could this also be used to implement the code for sharing to Instagram stories using Expo SDK?
In the official documentation it requires native code to use the pasteboard in iOS https://developers.facebook.com/docs/instagram/sharing-to-stories/#ios-developers

@nknaveen007
Copy link

we can create whatsapp stickers using react native managed workflow ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
iOS iOS issue offtopic This issue is offtopic. Might be closed soon.
Projects
None yet
Development

No branches or pull requests

5 participants