From e2d71a383bed923991a96637f21f3bf9f4da8836 Mon Sep 17 00:00:00 2001 From: Thorben Primke Date: Fri, 24 Apr 2020 13:16:15 -0700 Subject: [PATCH] [expo-sms] Adds Attachment Support To SMS --- apps/test-suite/tests/SMS.ios.js | 17 ++++ apps/test-suite/tests/SMS.js | 17 ++++ apps/test-suite/tests/SMSCommon.js | 98 +++++++++++++++++++ docs/pages/versions/unversioned/sdk/sms.md | 23 ++++- packages/expo-sms/CHANGELOG.md | 2 + packages/expo-sms/android/build.gradle | 1 + .../main/java/expo/modules/sms/SMSModule.java | 37 +++++-- packages/expo-sms/build/SMS.d.ts | 4 +- packages/expo-sms/build/SMS.js | 26 ++++- packages/expo-sms/build/SMS.js.map | 2 +- packages/expo-sms/build/SMS.types.d.ts | 8 ++ packages/expo-sms/build/SMS.types.js.map | 2 +- packages/expo-sms/ios/EXSMS/EXSMSModule.m | 39 +++++++- packages/expo-sms/src/SMS.ts | 32 +++++- packages/expo-sms/src/SMS.types.ts | 10 ++ packages/expo-sms/src/__tests__/SMS-test.ts | 33 ++++++- 16 files changed, 325 insertions(+), 26 deletions(-) create mode 100644 apps/test-suite/tests/SMSCommon.js diff --git a/apps/test-suite/tests/SMS.ios.js b/apps/test-suite/tests/SMS.ios.js index 8a59d04120f77..2a0b0a09395e7 100644 --- a/apps/test-suite/tests/SMS.ios.js +++ b/apps/test-suite/tests/SMS.ios.js @@ -3,6 +3,11 @@ import * as SMS from 'expo-sms'; import { expectMethodToThrowAsync } from '../TestUtils'; import { isInteractive } from '../utils/Environment'; +import { + testSMSComposeWithSingleImageAttachment, + testSMSComposeWithTwoImageAttachments, + testSMSComposeWithAudioAttachment, +} from './SMSCommon'; export const name = 'SMS'; @@ -14,6 +19,18 @@ export function test({ describe, it, expect }) { // TODO: Bacon: Build an API to close the UI Controller await SMS.sendSMSAsync(['0123456789', '9876543210'], 'test'); }); + + it(`opens an SMS composer with single image attachment`, async () => { + await testSMSComposeWithSingleImageAttachment(expect); + }); + + it(`opens an SMS composer with two image attachments`, async () => { + await testSMSComposeWithTwoImageAttachments(expect); + }); + + it(`opens an SMS composer with audio attachment`, async () => { + await testSMSComposeWithAudioAttachment(expect); + }); } } else { it(`is unavailable`, async () => { diff --git a/apps/test-suite/tests/SMS.js b/apps/test-suite/tests/SMS.js index 3eeea5d9d7019..a7937cb7977aa 100644 --- a/apps/test-suite/tests/SMS.js +++ b/apps/test-suite/tests/SMS.js @@ -2,6 +2,11 @@ import * as SMS from 'expo-sms'; import { Platform } from 'react-native'; import { isInteractive } from '../utils/Environment'; +import { + testSMSComposeWithSingleImageAttachment, + testSMSComposeWithAudioAttachment, + testSMSComposeWithTwoImageAttachments, +} from './SMSCommon'; export const name = 'SMS'; @@ -11,6 +16,18 @@ export function test({ describe, it, expect }) { it(`opens an SMS composer`, async () => { await SMS.sendSMSAsync(['0123456789', '9876543210'], 'test'); }); + + it(`opens an SMS composer with single image attachment`, async () => { + await testSMSComposeWithSingleImageAttachment(expect); + }); + + it(`opens an SMS composer with two image attachments. Only first one is used.`, async () => { + await testSMSComposeWithTwoImageAttachments(expect); + }); + + it(`opens an SMS composer with audio attachment`, async () => { + await testSMSComposeWithAudioAttachment(expect); + }); }); } diff --git a/apps/test-suite/tests/SMSCommon.js b/apps/test-suite/tests/SMSCommon.js new file mode 100644 index 0000000000000..d7b06bbb61869 --- /dev/null +++ b/apps/test-suite/tests/SMSCommon.js @@ -0,0 +1,98 @@ +import * as FS from 'expo-file-system'; +import * as SMS from 'expo-sms'; + +async function assertExists(testFile, expectedToExist, expect) { + const { exists } = await FS.getInfoAsync(testFile.localUri); + if (expectedToExist) { + expect(exists).toBeTruthy(); + } else { + expect(exists).not.toBeTruthy(); + } +} + +async function loadAndSaveFle(fileInfo, expect) { + await FS.deleteAsync(fileInfo.localUri, { idempotent: true }); + await assertExists(fileInfo, false, expect); + const { md5, headers } = await FS.downloadAsync(fileInfo.remoteUri, fileInfo.localUri, { + md5: true, + }); + expect(md5).toBe(fileInfo.md5); + await assertExists(fileInfo, true, expect); + expect(headers['Content-Type'] || headers['content-type']).toBe(fileInfo.mimeType); +} + +async function cleanupFile(fileInfo, expect) { + await FS.deleteAsync(fileInfo.localUri); + await assertExists(fileInfo, false, expect); +} + +const pngFile = { + localUri: FS.documentDirectory + 'image.png', + remoteUri: 'https://s3-us-west-1.amazonaws.com/test-suite-data/avatar2.png', + md5: '1e02045c10b8f1145edc7c8375998f87', + mimeType: 'image/png', +}; + +const gifFile = { + localUri: FS.documentDirectory + 'image.gif', + remoteUri: 'https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif', + md5: '090592ebd01ac1e425b2766989040f80', + mimeType: 'image/gif', +}; + +const audioFile = { + localUri: FS.documentDirectory + 'sound.mp3', + remoteUri: 'https://dl.espressif.com/dl/audio/gs-16b-1c-44100hz.mp3', + md5: 'e21d733c3506280974842f11c6d30005', + mimeType: 'audio/mpeg', +}; + +const numbers = ['0123456789', '9876543210']; + +export async function testSMSComposeWithSingleImageAttachment(expect) { + await loadAndSaveFle(pngFile, expect); + const contentUri = await FS.getContentUriAsync(pngFile.localUri); + await SMS.sendSMSAsync(numbers, 'test with image', { + attachments: { + uri: contentUri, + mimeType: 'image/png', + filename: 'image.png', + }, + }); + await cleanupFile(pngFile, expect); +} + +export async function testSMSComposeWithTwoImageAttachments(expect) { + await loadAndSaveFle(gifFile, expect); + await loadAndSaveFle(pngFile, expect); + await SMS.sendSMSAsync(numbers, 'test with two images', { + attachments: [ + { + uri: await FS.getContentUriAsync(gifFile.localUri), + mimeType: 'image/gif', + filename: 'image.gif', + }, + { + uri: await FS.getContentUriAsync(pngFile.localUri), + mimeType: 'image/png', + filename: 'image.png', + }, + ], + }); + await cleanupFile(gifFile, expect); + await cleanupFile(pngFile, expect); +} + +export async function testSMSComposeWithAudioAttachment(expect) { + await loadAndSaveFle(audioFile, expect); + await SMS.sendSMSAsync(numbers, 'test with audio', { + attachments: [ + { + uri: await FS.getContentUriAsync(audioFile.localUri), + mimeType: 'audio/mpeg', + filename: 'sound.mp3', + }, + ], + }); + await cleanupFile(audioFile, expect); +} diff --git a/docs/pages/versions/unversioned/sdk/sms.md b/docs/pages/versions/unversioned/sdk/sms.md index 050d1c8ff2994..c2541c73df6b3 100644 --- a/docs/pages/versions/unversioned/sdk/sms.md +++ b/docs/pages/versions/unversioned/sdk/sms.md @@ -47,7 +47,11 @@ Opens the default UI/app for sending SMS messages with prefilled addresses and m - **addresses (_Array\|string_)** -- An array of addresses (_phone numbers_) or single address passed as strings. Those would appear as recipients of the prepared message. -- **message (_string_)** -- Message to be sent +- **message (_string_)** -- Message to be sent. + +- **options (_optional_) (_object_)** -- A map defining additional sms configuration options + + - **attachments (_optional_) (_Array<\object_\>|_object_)** -- An array of [SMSAttachment](#smsattachment) objects or single object. Android supports only one attachment. #### Returns @@ -66,6 +70,21 @@ Android does not provide information about the status of the SMS message, so on ```javascript const { result } = await SMS.sendSMSAsync( ['0123456789', '9876543210'], - 'My sample HelloWorld message' + 'My sample HelloWorld message', + attachments: { + uri: 'path/myfile.png', + mimeType: 'image/png', + filename: 'myfile.png', + } ); ``` + +## Related types + +### SMSAttachment + +An object that is used to describe an attachment that is included with a SMS message. + +- **uri (_string_)** -- the content URI of the attachment. The URI needs be a content URI so that it can be accessed by other applications outside of Expo. (See [FileSystem.getContentUriAsync](../filesystem/#filesystemgetcontenturiasyncfileuri)) +- **mimeType (_string_)** -- the mime type of the attachment such as `image/png` +- **filename (_string_)** -- the filename of the attachment diff --git a/packages/expo-sms/CHANGELOG.md b/packages/expo-sms/CHANGELOG.md index 6a294c6841ff4..a1e4ba1d349b3 100644 --- a/packages/expo-sms/CHANGELOG.md +++ b/packages/expo-sms/CHANGELOG.md @@ -6,4 +6,6 @@ ### 🎉 New features +- Add `attachments` as an optional parameter to `sendSMSAsync`. It can be used to provide an attachment along with the recipients and message arguments. ([#7967](https://github.com/expo/expo/pull/7967) by [@thorbenprimke](https://github.com/thorbenprimke)) + ### 🐛 Bug fixes diff --git a/packages/expo-sms/android/build.gradle b/packages/expo-sms/android/build.gradle index eb97489aa6bfc..41a2d63f52d42 100644 --- a/packages/expo-sms/android/build.gradle +++ b/packages/expo-sms/android/build.gradle @@ -59,4 +59,5 @@ if (new File(rootProject.projectDir.parentFile, 'package.json').exists()) { dependencies { unimodule "unimodules-core" unimodule "unimodules-permissions-interface" + implementation 'androidx.annotation:annotation:1.1.0' } diff --git a/packages/expo-sms/android/src/main/java/expo/modules/sms/SMSModule.java b/packages/expo-sms/android/src/main/java/expo/modules/sms/SMSModule.java index 99f0c48610978..5925d60f31ca4 100644 --- a/packages/expo-sms/android/src/main/java/expo/modules/sms/SMSModule.java +++ b/packages/expo-sms/android/src/main/java/expo/modules/sms/SMSModule.java @@ -5,9 +5,11 @@ import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; +import android.provider.Telephony; import java.util.ArrayList; import java.util.List; +import java.util.Map; import org.unimodules.core.ExportedModule; import org.unimodules.core.ModuleRegistry; @@ -17,10 +19,14 @@ import org.unimodules.core.interfaces.LifecycleEventListener; import org.unimodules.core.interfaces.services.UIManager; +import androidx.annotation.Nullable; + public class SMSModule extends ExportedModule implements LifecycleEventListener { private static final String TAG = "ExpoSMS"; private static final String ERROR_TAG = "E_SMS"; + private static final String OPTIONS_ATTACHMENTS_KEY = "attachments"; + private ModuleRegistry mModuleRegistry; private Promise mPendingPromise; private boolean mSMSComposerOpened = false; @@ -53,23 +59,42 @@ public void onDestroy() { } @ExpoMethod - public void sendSMSAsync(final ArrayList addresses, final String message, final Promise promise) { + public void sendSMSAsync( + final ArrayList addresses, + final String message, + final @Nullable Map options, + final Promise promise) { if (mPendingPromise != null) { promise.reject(ERROR_TAG + "_SENDING_IN_PROGRESS", "Different SMS sending in progress. Await the old request and then try again."); return; } - final Intent SMSIntent = new Intent(Intent.ACTION_SENDTO); + Intent SMSIntent = new Intent(Intent.ACTION_SEND); + String defaultSMSPackage = Telephony.Sms.getDefaultSmsPackage(getContext()); + if (defaultSMSPackage != null){ + SMSIntent.setPackage(defaultSMSPackage); + } else { + promise.reject(ERROR_TAG + "_NO_SMS_APP", "No messaging application available"); + return; + } + SMSIntent.setType("text/plain"); final String smsTo = constructRecipients(addresses); - SMSIntent.setData(Uri.parse("smsto:" + smsTo)); + SMSIntent.putExtra("address", smsTo); + SMSIntent.putExtra("exit_on_sent", true); SMSIntent.putExtra("compose_mode", true); SMSIntent.putExtra(Intent.EXTRA_TEXT, message); SMSIntent.putExtra("sms_body", message); - if (SMSIntent.resolveActivity(getContext().getPackageManager()) == null) { - promise.reject(ERROR_TAG + "_NO_SMS_APP", "No messaging application available"); - return; + if (options != null) { + if (options.containsKey(OPTIONS_ATTACHMENTS_KEY)) { + final List> attachments = (List>) options.get(OPTIONS_ATTACHMENTS_KEY); + if (attachments != null && !attachments.isEmpty()) { + Map attachment = attachments.get(0); + SMSIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse(attachment.get("uri"))); + SMSIntent.setType(attachment.get("mimeType")); + } + } } mPendingPromise = promise; diff --git a/packages/expo-sms/build/SMS.d.ts b/packages/expo-sms/build/SMS.d.ts index c6733f81d0a76..a0fe28b0910f7 100644 --- a/packages/expo-sms/build/SMS.d.ts +++ b/packages/expo-sms/build/SMS.d.ts @@ -1,6 +1,6 @@ -import { SMSResponse } from './SMS.types'; +import { SMSResponse, SMSOptions } from './SMS.types'; export { SMSResponse }; -export declare function sendSMSAsync(addresses: string | string[], message: string): Promise; +export declare function sendSMSAsync(addresses: string | string[], message: string, options?: SMSOptions): Promise; /** * The device has a telephony radio with data communication support. * - Always returns `false` in the iOS simulator, and browser diff --git a/packages/expo-sms/build/SMS.js b/packages/expo-sms/build/SMS.js index 99ec2c0e6ae07..6ed6ab7e573af 100644 --- a/packages/expo-sms/build/SMS.js +++ b/packages/expo-sms/build/SMS.js @@ -1,11 +1,29 @@ -import { UnavailabilityError } from '@unimodules/core'; +/* eslint-disable no-unused-expressions */ +import { UnavailabilityError, Platform } from '@unimodules/core'; import ExpoSMS from './ExpoSMS'; -export async function sendSMSAsync(addresses, message) { - const finalAddresses = Array.isArray(addresses) ? addresses : [addresses]; +function processAttachments(attachments) { + if (!attachments) { + return null; + } + attachments = Array.isArray(attachments) ? attachments : [attachments]; + if (Platform.OS === 'android' && attachments.length > 1) { + console.warn('Android only supports a single attachment. The first array item is used.'); + attachments = attachments.slice(0, 1); + } + return attachments; +} +export async function sendSMSAsync(addresses, message, options) { if (!ExpoSMS.sendSMSAsync) { throw new UnavailabilityError('expo-sms', 'sendSMSAsync'); } - return ExpoSMS.sendSMSAsync(finalAddresses, message); + const finalAddresses = Array.isArray(addresses) ? addresses : [addresses]; + const finalOptions = { + ...options, + }; + if (options?.attachments) { + finalOptions.attachments = processAttachments(options?.attachments) || undefined; + } + return ExpoSMS.sendSMSAsync(finalAddresses, message, finalOptions); } /** * The device has a telephony radio with data communication support. diff --git a/packages/expo-sms/build/SMS.js.map b/packages/expo-sms/build/SMS.js.map index b3eaca58cb15a..a456b6959c7b2 100644 --- a/packages/expo-sms/build/SMS.js.map +++ b/packages/expo-sms/build/SMS.js.map @@ -1 +1 @@ -{"version":3,"file":"SMS.js","sourceRoot":"","sources":["../src/SMS.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD,OAAO,OAAO,MAAM,WAAW,CAAC;AAKhC,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,SAA4B,EAC5B,OAAe;IAEf,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC1E,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;QACzB,MAAM,IAAI,mBAAmB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;KAC3D;IACD,OAAO,OAAO,CAAC,YAAY,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;AACvD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,OAAO,CAAC,gBAAgB,EAAE,CAAC;AACpC,CAAC","sourcesContent":["import { UnavailabilityError } from '@unimodules/core';\n\nimport ExpoSMS from './ExpoSMS';\nimport { SMSResponse } from './SMS.types';\n\nexport { SMSResponse };\n\nexport async function sendSMSAsync(\n addresses: string | string[],\n message: string\n): Promise {\n const finalAddresses = Array.isArray(addresses) ? addresses : [addresses];\n if (!ExpoSMS.sendSMSAsync) {\n throw new UnavailabilityError('expo-sms', 'sendSMSAsync');\n }\n return ExpoSMS.sendSMSAsync(finalAddresses, message);\n}\n\n/**\n * The device has a telephony radio with data communication support.\n * - Always returns `false` in the iOS simulator, and browser\n */\nexport async function isAvailableAsync(): Promise {\n return ExpoSMS.isAvailableAsync();\n}\n"]} \ No newline at end of file +{"version":3,"file":"SMS.js","sourceRoot":"","sources":["../src/SMS.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,OAAO,EAAE,mBAAmB,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjE,OAAO,OAAO,MAAM,WAAW,CAAC;AAKhC,SAAS,kBAAkB,CACzB,WAAwD;IAExD,IAAI,CAAC,WAAW,EAAE;QAChB,OAAO,IAAI,CAAC;KACb;IACD,WAAW,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC;IACvE,IAAI,QAAQ,CAAC,EAAE,KAAK,SAAS,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE;QACvD,OAAO,CAAC,IAAI,CAAC,0EAA0E,CAAC,CAAC;QACzF,WAAW,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;KACvC;IACD,OAAO,WAAW,CAAC;AACrB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,SAA4B,EAC5B,OAAe,EACf,OAAoB;IAEpB,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE;QACzB,MAAM,IAAI,mBAAmB,CAAC,UAAU,EAAE,cAAc,CAAC,CAAC;KAC3D;IACD,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC1E,MAAM,YAAY,GAAG;QACnB,GAAG,OAAO;KACG,CAAC;IAChB,IAAI,OAAO,EAAE,WAAW,EAAE;QACxB,YAAY,CAAC,WAAW,GAAG,kBAAkB,CAAC,OAAO,EAAE,WAAW,CAAC,IAAI,SAAS,CAAC;KAClF;IACD,OAAO,OAAO,CAAC,YAAY,CAAC,cAAc,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC;AACrE,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,OAAO,CAAC,gBAAgB,EAAE,CAAC;AACpC,CAAC","sourcesContent":["/* eslint-disable no-unused-expressions */\nimport { UnavailabilityError, Platform } from '@unimodules/core';\n\nimport ExpoSMS from './ExpoSMS';\nimport { SMSAttachment, SMSResponse, SMSOptions } from './SMS.types';\n\nexport { SMSResponse };\n\nfunction processAttachments(\n attachments: SMSAttachment | SMSAttachment[] | undefined\n): SMSAttachment[] | null {\n if (!attachments) {\n return null;\n }\n attachments = Array.isArray(attachments) ? attachments : [attachments];\n if (Platform.OS === 'android' && attachments.length > 1) {\n console.warn('Android only supports a single attachment. The first array item is used.');\n attachments = attachments.slice(0, 1);\n }\n return attachments;\n}\n\nexport async function sendSMSAsync(\n addresses: string | string[],\n message: string,\n options?: SMSOptions\n): Promise {\n if (!ExpoSMS.sendSMSAsync) {\n throw new UnavailabilityError('expo-sms', 'sendSMSAsync');\n }\n const finalAddresses = Array.isArray(addresses) ? addresses : [addresses];\n const finalOptions = {\n ...options,\n } as SMSOptions;\n if (options?.attachments) {\n finalOptions.attachments = processAttachments(options?.attachments) || undefined;\n }\n return ExpoSMS.sendSMSAsync(finalAddresses, message, finalOptions);\n}\n\n/**\n * The device has a telephony radio with data communication support.\n * - Always returns `false` in the iOS simulator, and browser\n */\nexport async function isAvailableAsync(): Promise {\n return ExpoSMS.isAvailableAsync();\n}\n"]} \ No newline at end of file diff --git a/packages/expo-sms/build/SMS.types.d.ts b/packages/expo-sms/build/SMS.types.d.ts index 556e1d91e77c1..831ed42073953 100644 --- a/packages/expo-sms/build/SMS.types.d.ts +++ b/packages/expo-sms/build/SMS.types.d.ts @@ -1,3 +1,11 @@ export declare type SMSResponse = { result: 'unknown' | 'sent' | 'cancelled'; }; +export declare type SMSAttachment = { + uri: string; + mimeType: string; + filename: string; +}; +export declare type SMSOptions = { + attachments?: SMSAttachment | SMSAttachment[] | undefined; +}; diff --git a/packages/expo-sms/build/SMS.types.js.map b/packages/expo-sms/build/SMS.types.js.map index 4e17acd2db5dc..61c3797929021 100644 --- a/packages/expo-sms/build/SMS.types.js.map +++ b/packages/expo-sms/build/SMS.types.js.map @@ -1 +1 @@ -{"version":3,"file":"SMS.types.js","sourceRoot":"","sources":["../src/SMS.types.ts"],"names":[],"mappings":"","sourcesContent":["export type SMSResponse = {\n result: 'unknown' | 'sent' | 'cancelled';\n};\n"]} \ No newline at end of file +{"version":3,"file":"SMS.types.js","sourceRoot":"","sources":["../src/SMS.types.ts"],"names":[],"mappings":"","sourcesContent":["export type SMSResponse = {\n result: 'unknown' | 'sent' | 'cancelled';\n};\n\nexport type SMSAttachment = {\n uri: string;\n mimeType: string;\n filename: string;\n};\n\nexport type SMSOptions = {\n attachments?: SMSAttachment | SMSAttachment[] | undefined;\n};\n"]} \ No newline at end of file diff --git a/packages/expo-sms/ios/EXSMS/EXSMSModule.m b/packages/expo-sms/ios/EXSMS/EXSMSModule.m index cc05df8416e1a..88251e561e99e 100644 --- a/packages/expo-sms/ios/EXSMS/EXSMSModule.m +++ b/packages/expo-sms/ios/EXSMS/EXSMSModule.m @@ -3,6 +3,11 @@ #import #import #import +#if SD_MAC +#import +#else +#import +#endif @interface EXSMSModule () @@ -31,6 +36,7 @@ - (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry UM_EXPORT_METHOD_AS(sendSMSAsync, sendSMS:(NSArray *)addresses message:(NSString *)message + options:(NSDictionary *)options resolver:(UMPromiseResolveBlock)resolve rejecter:(UMPromiseRejectBlock)reject) { @@ -51,7 +57,36 @@ - (void)setModuleRegistry:(UMModuleRegistry *)moduleRegistry messageComposeViewController.messageComposeDelegate = self; messageComposeViewController.recipients = addresses; messageComposeViewController.body = message; - + + if (options) { + if (options[@"attachments"]) { + NSArray *attachments = (NSArray *) [options objectForKey:@"attachments"]; + for (NSDictionary* attachment in attachments) { + NSString *mimeType = attachment[@"mimeType"]; + CFStringRef mimeTypeRef = (__bridge CFStringRef)mimeType; + CFStringRef utiRef = UTTypeCreatePreferredIdentifierForTag(kUTTagClassMIMEType, mimeTypeRef, NULL); + if (utiRef == NULL) { + reject(@"E_SMS_ATTACHMENT", [NSString stringWithFormat:@"Failed to find UTI for mimeType: %@", mimeType], nil); + _resolve = nil; + _reject = nil; + return; + } + NSString *typeIdentifier = (__bridge_transfer NSString *)utiRef; + NSString *uri = attachment[@"uri"]; + NSString *filename = attachment[@"filename"]; + NSError *error; + NSData *attachmentData = [NSData dataWithContentsOfURL:[NSURL URLWithString:uri] options:(NSDataReadingOptions)0 error:&error]; + bool attached = [messageComposeViewController addAttachmentData:attachmentData typeIdentifier:typeIdentifier filename:filename]; + if (!attached) { + reject(@"E_SMS_ATTACHMENT", [NSString stringWithFormat:@"Failed to attach file: %@", uri], nil); + _resolve = nil; + _reject = nil; + return; + } + } + } + } + UM_WEAKIFY(self); [UMUtilities performSynchronouslyOnMainThread:^{ UM_ENSURE_STRONGIFY(self); @@ -90,5 +125,5 @@ - (void)messageComposeViewController:(MFMessageComposeViewController *)controlle self->_resolve = nil; }]; } - + @end diff --git a/packages/expo-sms/src/SMS.ts b/packages/expo-sms/src/SMS.ts index 752072015886f..dad42a88393a4 100644 --- a/packages/expo-sms/src/SMS.ts +++ b/packages/expo-sms/src/SMS.ts @@ -1,19 +1,41 @@ -import { UnavailabilityError } from '@unimodules/core'; +/* eslint-disable no-unused-expressions */ +import { UnavailabilityError, Platform } from '@unimodules/core'; import ExpoSMS from './ExpoSMS'; -import { SMSResponse } from './SMS.types'; +import { SMSAttachment, SMSResponse, SMSOptions } from './SMS.types'; export { SMSResponse }; +function processAttachments( + attachments: SMSAttachment | SMSAttachment[] | undefined +): SMSAttachment[] | null { + if (!attachments) { + return null; + } + attachments = Array.isArray(attachments) ? attachments : [attachments]; + if (Platform.OS === 'android' && attachments.length > 1) { + console.warn('Android only supports a single attachment. The first array item is used.'); + attachments = attachments.slice(0, 1); + } + return attachments; +} + export async function sendSMSAsync( addresses: string | string[], - message: string + message: string, + options?: SMSOptions ): Promise { - const finalAddresses = Array.isArray(addresses) ? addresses : [addresses]; if (!ExpoSMS.sendSMSAsync) { throw new UnavailabilityError('expo-sms', 'sendSMSAsync'); } - return ExpoSMS.sendSMSAsync(finalAddresses, message); + const finalAddresses = Array.isArray(addresses) ? addresses : [addresses]; + const finalOptions = { + ...options, + } as SMSOptions; + if (options?.attachments) { + finalOptions.attachments = processAttachments(options?.attachments) || undefined; + } + return ExpoSMS.sendSMSAsync(finalAddresses, message, finalOptions); } /** diff --git a/packages/expo-sms/src/SMS.types.ts b/packages/expo-sms/src/SMS.types.ts index 587ae232c4e2e..694544adbe39d 100644 --- a/packages/expo-sms/src/SMS.types.ts +++ b/packages/expo-sms/src/SMS.types.ts @@ -1,3 +1,13 @@ export type SMSResponse = { result: 'unknown' | 'sent' | 'cancelled'; }; + +export type SMSAttachment = { + uri: string; + mimeType: string; + filename: string; +}; + +export type SMSOptions = { + attachments?: SMSAttachment | SMSAttachment[] | undefined; +}; diff --git a/packages/expo-sms/src/__tests__/SMS-test.ts b/packages/expo-sms/src/__tests__/SMS-test.ts index 888b998d77ddb..42265c2d27d19 100644 --- a/packages/expo-sms/src/__tests__/SMS-test.ts +++ b/packages/expo-sms/src/__tests__/SMS-test.ts @@ -1,13 +1,40 @@ -import { NativeModulesProxy } from '@unimodules/core'; +import { NativeModulesProxy, Platform } from '@unimodules/core'; import * as SMS from '../SMS'; +import { SMSAttachment } from '../SMS.types'; it(`normalizes one phone number into an array`, async () => { const { ExpoSMS } = NativeModulesProxy; await SMS.sendSMSAsync('0123456789', 'test'); - expect(ExpoSMS.sendSMSAsync).toHaveBeenLastCalledWith(['0123456789'], 'test'); + expect(ExpoSMS.sendSMSAsync).toHaveBeenLastCalledWith(['0123456789'], 'test', {}); await SMS.sendSMSAsync(['0123456789', '9876543210'], 'test'); - expect(ExpoSMS.sendSMSAsync).toHaveBeenLastCalledWith(['0123456789', '9876543210'], 'test'); + expect(ExpoSMS.sendSMSAsync).toHaveBeenLastCalledWith(['0123456789', '9876543210'], 'test', {}); +}); + +it(`normalizes attachments parameter to always pass array to native`, async () => { + const { ExpoSMS } = NativeModulesProxy; + + const imageAttachment = { + uri: 'path/myfile.png', + filename: 'myfile.png', + mimeType: 'image/png', + } as SMSAttachment; + + await SMS.sendSMSAsync('0123456789', 'test', { attachments: imageAttachment }); + expect(ExpoSMS.sendSMSAsync).toHaveBeenLastCalledWith(['0123456789'], 'test', { + attachments: [imageAttachment], + }); + + const audioAttachment = { + uri: 'path/myfile.mp3', + filename: 'myfile.mp3', + mimeType: 'image/mp3', + } as SMSAttachment; + const multipleAttachments = [imageAttachment, audioAttachment]; + await SMS.sendSMSAsync('0123456789', 'test', { attachments: multipleAttachments }); + expect(ExpoSMS.sendSMSAsync).toHaveBeenLastCalledWith(['0123456789'], 'test', { + attachments: Platform.OS === 'android' ? [imageAttachment] : multipleAttachments, + }); });