WhatsApp / stickers

This repository contains the iOS and Android sample apps and API for creating third party sticker packs for WhatsApp.
Other
2.7k stars 1.69k forks source link

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

Open EvanBacon opened 4 years ago

EvanBacon commented 4 years ago

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);
}
emiliocortina commented 4 years ago

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 commented 3 years ago

we can create whatsapp stickers using react native managed workflow ?